injectdi 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Hossam Hamdy
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Inject DI
2
+
3
+ High-performant, decorator-free dependency IoC container for Node.js.
4
+
5
+ > **Status: pre-alpha (`0.0.x`).** The public API is being designed in the open. Implementation is incomplete and breaking changes will land on every release until `0.1.0`. Do not use in production yet.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install injectdi
11
+
12
+ pnpm install injectdi
13
+
14
+ yarn install injectdi
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ - Node.js `>=22.12.0`
20
+ - ESM only — no CommonJS build is shipped. Consumers on Node `>=22.12.0` can still `require()` it via Node's built-in `require(esm)` interop.
21
+
22
+ ## License
23
+
24
+ [ISC](./LICENSE) © Hossam Hamdy
@@ -0,0 +1,9 @@
1
+ import type { Lifetime } from "./lifetime.ts";
2
+ export declare const EMPTY: unique symbol;
3
+ export declare const CIRCULAR: unique symbol;
4
+ export interface Binding<T = unknown> {
5
+ factory: (() => T) | undefined;
6
+ value: T | typeof EMPTY | typeof CIRCULAR;
7
+ lifetime: Lifetime;
8
+ }
9
+ export declare function createBinding<T>(factory: (() => T) | undefined, value: T | typeof EMPTY, lifetime: Lifetime): Binding<T>;
@@ -0,0 +1,5 @@
1
+ export const EMPTY = Symbol("EMPTY");
2
+ export const CIRCULAR = Symbol("CIRCULAR");
3
+ export function createBinding(factory, value, lifetime) {
4
+ return { factory, value, lifetime };
5
+ }
@@ -0,0 +1,22 @@
1
+ import type { Token } from "./token.ts";
2
+ /** Throws an error when a token is not found. */
3
+ export declare class ProviderNotFoundError extends Error {
4
+ readonly name = "ProviderNotFoundError";
5
+ readonly scopeName?: string;
6
+ constructor(token: Token, scopeName?: string);
7
+ }
8
+ /** circular dependency runtime error. */
9
+ export declare class CircularDependencyError extends Error {
10
+ readonly name = "CircularDependencyError";
11
+ constructor(token: Token);
12
+ }
13
+ /** inject() called outside of a factory or runInInjectionContext. */
14
+ export declare class InjectionContextError extends Error {
15
+ readonly name = "InjectionContextError";
16
+ constructor(token?: Token);
17
+ }
18
+ /** Container or scope has already been destroyed. */
19
+ export declare class ScopeDestroyedError extends Error {
20
+ readonly name = "ScopeDestroyedError";
21
+ constructor(name?: string);
22
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ import { tokenName } from "./token.js";
2
+ /** Throws an error when a token is not found. */
3
+ export class ProviderNotFoundError extends Error {
4
+ name = "ProviderNotFoundError";
5
+ scopeName;
6
+ constructor(token, scopeName) {
7
+ super(`Provider not found for: ${tokenName(token)}.`);
8
+ this.scopeName = scopeName;
9
+ }
10
+ }
11
+ /** circular dependency runtime error. */
12
+ export class CircularDependencyError extends Error {
13
+ name = "CircularDependencyError";
14
+ constructor(token) {
15
+ super(`Circular dependency detected for: ${tokenName(token)}.`);
16
+ }
17
+ }
18
+ /** inject() called outside of a factory or runInInjectionContext. */
19
+ export class InjectionContextError extends Error {
20
+ name = "InjectionContextError";
21
+ constructor(token) {
22
+ super(`inject(${token ? `${tokenName(token)}` : ""}) called outside of an injection context. ` +
23
+ "can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`");
24
+ }
25
+ }
26
+ /** Container or scope has already been destroyed. */
27
+ export class ScopeDestroyedError extends Error {
28
+ name = "ScopeDestroyedError";
29
+ constructor(name = "Scope") {
30
+ super(`${name} has already been destroyed.`);
31
+ }
32
+ }
@@ -0,0 +1,8 @@
1
+ export { CircularDependencyError, InjectionContextError, ProviderNotFoundError, ScopeDestroyedError, } from "./errors.ts";
2
+ export { inject, runInInjectionContext } from "./injection-context.ts";
3
+ export { Lifetime } from "./lifetime.ts";
4
+ export type { ClassProvider, ExistingProvider, FactoryProvider, Provider, ValueProvider, } from "./provider.ts";
5
+ export type { ResolveOptions, Resolver } from "./resolver.ts";
6
+ export { Scope } from "./scope.ts";
7
+ export type { AbstractClass, Class, Token } from "./token.ts";
8
+ export { InjectionToken } from "./token.ts";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Errors
2
+ export { CircularDependencyError, InjectionContextError, ProviderNotFoundError, ScopeDestroyedError, } from "./errors.js";
3
+ // Injection context
4
+ export { inject, runInInjectionContext } from "./injection-context.js";
5
+ export { Lifetime } from "./lifetime.js";
6
+ // Scope
7
+ export { Scope } from "./scope.js";
8
+ export { InjectionToken } from "./token.js";
@@ -0,0 +1,24 @@
1
+ import type { Resolver } from "./resolver.ts";
2
+ import type { Token } from "./token.ts";
3
+ /** @internal */
4
+ export declare function getInjectionContext(): Resolver | undefined;
5
+ /** @internal — Returns the previous context so it can be restored. */
6
+ export declare function setInjectionContext(resolver: Resolver | undefined): Resolver | undefined;
7
+ /**
8
+ * Retrieve a dependency from the currently active injection context.
9
+ *
10
+ * Must be called **synchronously** during container-managed construction:
11
+ * inside a factory, inside a class field initializer triggered by a
12
+ * `useClass` provider, or inside `runInInjectionContext()`.
13
+ */
14
+ export declare function inject<T>(token: Token<T>): T;
15
+ export declare function inject<T>(token: Token<T>, options: {
16
+ optional: true;
17
+ }): T | null;
18
+ /**
19
+ * Run a function with a specific resolver as the active injection context.
20
+ *
21
+ * Within `fn`, calls to `inject()` will resolve against `resolver`.
22
+ * The previous context (if any) is restored when `fn` returns.
23
+ */
24
+ export declare function runInInjectionContext<R>(resolver: Resolver, fn: () => R): R;
@@ -0,0 +1,34 @@
1
+ import { InjectionContextError } from "./errors.js";
2
+ let _currentContext;
3
+ /** @internal */
4
+ export function getInjectionContext() {
5
+ return _currentContext;
6
+ }
7
+ /** @internal — Returns the previous context so it can be restored. */
8
+ export function setInjectionContext(resolver) {
9
+ const prev = _currentContext;
10
+ _currentContext = resolver;
11
+ return prev;
12
+ }
13
+ export function inject(token, options) {
14
+ const resolver = getInjectionContext();
15
+ if (!resolver) {
16
+ throw new InjectionContextError(token);
17
+ }
18
+ return resolver.resolve(token, options);
19
+ }
20
+ /**
21
+ * Run a function with a specific resolver as the active injection context.
22
+ *
23
+ * Within `fn`, calls to `inject()` will resolve against `resolver`.
24
+ * The previous context (if any) is restored when `fn` returns.
25
+ */
26
+ export function runInInjectionContext(resolver, fn) {
27
+ const prev = setInjectionContext(resolver);
28
+ try {
29
+ return fn();
30
+ }
31
+ finally {
32
+ setInjectionContext(prev);
33
+ }
34
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Defines how long an instance lives and how it is shared.
3
+ */
4
+ export declare const Lifetime: Readonly<{
5
+ /** One instance for the entire container. */
6
+ Singleton: "singleton";
7
+ /** New instance per injection point. */
8
+ Transient: "transient";
9
+ }>;
10
+ export type Lifetime = (typeof Lifetime)[keyof typeof Lifetime];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Defines how long an instance lives and how it is shared.
3
+ */
4
+ export const Lifetime = Object.freeze({
5
+ /** One instance for the entire container. */
6
+ Singleton: "singleton",
7
+ /** New instance per injection point. */
8
+ Transient: "transient",
9
+ });
@@ -0,0 +1,39 @@
1
+ import type { Lifetime } from "./lifetime.ts";
2
+ import type { Class, Token } from "./token.ts";
3
+ /**
4
+ * Provide a pre-existing value. No factory, no lifetime — the value is
5
+ * always immediately available and lives as long as its container.
6
+ */
7
+ export interface ValueProvider<T = unknown> {
8
+ provide: Token<T>;
9
+ useValue: T;
10
+ }
11
+ /**
12
+ * Provide via a factory function. `inject()` works inside the factory.
13
+ */
14
+ export interface FactoryProvider<T = unknown> {
15
+ provide: Token<T>;
16
+ useFactory: () => T;
17
+ lifetime?: Lifetime;
18
+ }
19
+ /**
20
+ * Provide by constructing a class. `inject()` works in field initializers.
21
+ */
22
+ export interface ClassProvider<T = unknown> {
23
+ provide: Token<T>;
24
+ useClass: Class<T>;
25
+ lifetime?: Lifetime;
26
+ }
27
+ /**
28
+ * Alias: resolve one token as another.
29
+ * Inherits the lifetime of the target token's registration.
30
+ */
31
+ export interface ExistingProvider<T = unknown> {
32
+ provide: Token<T>;
33
+ useExisting: Token<T>;
34
+ }
35
+ export type Provider<T = unknown> = ValueProvider<T> | FactoryProvider<T> | ClassProvider<T> | ExistingProvider<T>;
36
+ export declare function isValueProvider<T>(p: Provider<T>): p is ValueProvider<T>;
37
+ export declare function isFactoryProvider<T>(p: Provider<T>): p is FactoryProvider<T>;
38
+ export declare function isClassProvider<T>(p: Provider<T>): p is ClassProvider<T>;
39
+ export declare function isExistingProvider<T>(p: Provider<T>): p is ExistingProvider<T>;
@@ -0,0 +1,12 @@
1
+ export function isValueProvider(p) {
2
+ return "useValue" in p;
3
+ }
4
+ export function isFactoryProvider(p) {
5
+ return "useFactory" in p;
6
+ }
7
+ export function isClassProvider(p) {
8
+ return "useClass" in p;
9
+ }
10
+ export function isExistingProvider(p) {
11
+ return "useExisting" in p;
12
+ }
@@ -0,0 +1,11 @@
1
+ import type { Token } from "./token.ts";
2
+ export interface ResolveOptions {
3
+ optional?: boolean;
4
+ }
5
+ export interface Resolver {
6
+ resolve<T>(token: Token<T>): T;
7
+ resolve<T>(token: Token<T>, options: {
8
+ optional: true;
9
+ }): T | null;
10
+ resolve<T>(token: Token<T>, options?: ResolveOptions): T | null;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { type Provider } from "./provider.ts";
2
+ import type { Resolver } from "./resolver.ts";
3
+ import type { Token } from "./token.ts";
4
+ export declare class Scope implements Resolver, AsyncDisposable {
5
+ #private;
6
+ constructor(providers: Provider[]);
7
+ get disposed(): boolean;
8
+ resolve<T>(token: Token<T>): T;
9
+ resolve<T>(token: Token<T>, options: {
10
+ optional: true;
11
+ }): T | null;
12
+ has<T>(token: Token<T>): boolean;
13
+ dispose(): Promise<void>;
14
+ [Symbol.asyncDispose](): Promise<void>;
15
+ }
package/dist/scope.js ADDED
@@ -0,0 +1,44 @@
1
+ import { createBinding, EMPTY } from "./binding.js";
2
+ import { inject } from "./injection-context.js";
3
+ import { Lifetime } from "./lifetime.js";
4
+ import { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, } from "./provider.js";
5
+ export class Scope {
6
+ #disposed;
7
+ #bindings;
8
+ constructor(providers) {
9
+ this.#disposed = false;
10
+ this.#bindings = new Map();
11
+ for (const provider of providers) {
12
+ this.#bindings.set(provider.provide, providerToBinding(provider));
13
+ }
14
+ }
15
+ get disposed() {
16
+ return this.#disposed;
17
+ }
18
+ resolve(_token, _options) {
19
+ throw new Error("Scope.resolve is not implemented in this preview build");
20
+ }
21
+ has(token) {
22
+ return this.#bindings.has(token);
23
+ }
24
+ async dispose() {
25
+ this.#disposed = true;
26
+ }
27
+ [Symbol.asyncDispose]() {
28
+ return this.dispose();
29
+ }
30
+ }
31
+ function providerToBinding(provider) {
32
+ switch (true) {
33
+ case isValueProvider(provider):
34
+ return createBinding(undefined, provider.useValue, Lifetime.Singleton);
35
+ case isExistingProvider(provider):
36
+ return createBinding(() => inject(provider.useExisting), EMPTY, Lifetime.Transient);
37
+ case isFactoryProvider(provider):
38
+ return createBinding(provider.useFactory, EMPTY, provider.lifetime ?? Lifetime.Singleton);
39
+ case isClassProvider(provider):
40
+ return createBinding(() => new provider.useClass(), EMPTY, provider.lifetime ?? Lifetime.Singleton);
41
+ default:
42
+ throw new Error("Invalid provider type", { cause: provider });
43
+ }
44
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A token that can be used as a DI key for non-class dependencies
3
+ * (strings, numbers, interfaces, configuration objects, etc.).
4
+ *
5
+ * Each `InjectionToken` instance is unique by identity — two tokens
6
+ * with the same description are still different tokens.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const API_URL = new InjectionToken<string>('API_URL');
11
+ * ```
12
+ */
13
+ export declare class InjectionToken<T> {
14
+ readonly __type: T;
15
+ readonly description: string;
16
+ constructor(description: string);
17
+ toString(): string;
18
+ }
19
+ /** A concrete class constructor. */
20
+ export type Class<T = unknown> = new (...args: unknown[]) => T;
21
+ /** A class or abstract class reference. */
22
+ export type AbstractClass<T = unknown> = abstract new (...args: unknown[]) => T;
23
+ /** Anything that can be used as a DI key. */
24
+ export type Token<T = unknown> = InjectionToken<T> | AbstractClass<T>;
25
+ /** Returns a human-readable name for a token. */
26
+ export declare function tokenName(token: Token): string;
package/dist/token.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A token that can be used as a DI key for non-class dependencies
3
+ * (strings, numbers, interfaces, configuration objects, etc.).
4
+ *
5
+ * Each `InjectionToken` instance is unique by identity — two tokens
6
+ * with the same description are still different tokens.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const API_URL = new InjectionToken<string>('API_URL');
11
+ * ```
12
+ */
13
+ export class InjectionToken {
14
+ description;
15
+ constructor(description) {
16
+ this.description = description;
17
+ }
18
+ toString() {
19
+ return `InjectionToken(${this.description})`;
20
+ }
21
+ }
22
+ /** Returns a human-readable name for a token. */
23
+ export function tokenName(token) {
24
+ if (token instanceof InjectionToken)
25
+ return token.toString();
26
+ return token.name || String(token);
27
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "injectdi",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "High-performant, decorator-free dependency IoC container for Node.js.",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "sideEffects": false,
21
+ "engines": {
22
+ "node": ">=22.12.0"
23
+ },
24
+ "keywords": [
25
+ "di",
26
+ "dependency-injection",
27
+ "ioc",
28
+ "ioc-container",
29
+ "inversion-of-control",
30
+ "container",
31
+ "decorator-free",
32
+ "scope",
33
+ "binding",
34
+ "token",
35
+ "typescript",
36
+ "esm",
37
+ "node"
38
+ ],
39
+ "license": "ISC",
40
+ "author": "Hossam Hamdy <hossamhamdy117@gmail.com> (https://github.com/hossam7amdy)",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/hossam7amdy/injectdi.git"
44
+ },
45
+ "homepage": "https://github.com/hossam7amdy/injectdi#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/hossam7amdy/injectdi/issues"
48
+ },
49
+ "scripts": {
50
+ "preinstall": "npx only-allow pnpm",
51
+ "clean": "rm -rf dist",
52
+ "build": "tsc -p tsconfig.build.json",
53
+ "prepublishOnly": "pnpm run clean && pnpm run check && pnpm run test && pnpm run build",
54
+ "test": "node --test 'src/__tests__/**/*.test.ts'",
55
+ "check": "biome check .",
56
+ "check:fix": "biome check . --write"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "devDependencies": {
62
+ "@biomejs/biome": "^2.4.15",
63
+ "@types/node": "^22.19.19",
64
+ "typescript": "^6.0.3"
65
+ },
66
+ "packageManager": "pnpm@11.1.2"
67
+ }