syrinject 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,5 +1,208 @@
1
1
  <p align="center">
2
- <img src="https://cdn.jsdelivr.net/npm/syrinject@latest/assets/logo-without-background.png" alt="Syrinject Logo" width="500" />
2
+ <img src="https://cdn.jsdelivr.net/npm/syrinject@latest/assets/Syringect-Black.png" alt="Syrinject Logo" width="500" />
3
3
  </p>
4
4
 
5
- <hr>
5
+ <p align="center">
6
+ <strong>A tiny, modern dependency injection container for TypeScript.</strong>
7
+ </p>
8
+
9
+ <hr>
10
+
11
+ ## Overview
12
+
13
+ Syrinject is a lightweight Dependency Injection (DI) container built for TypeScript. It focuses on clarity and explicitness while remaining flexible enough for small apps and larger codebases. You can register providers and resolve dependencies by token.
14
+
15
+ ## Features
16
+
17
+ - ✅ Simple DI container API.
18
+ - ✅ Typed tokens for safe resolution.
19
+ - ✅ Class, value, and factory providers.
20
+ - ✅ Singleton or transient lifetimes.
21
+ - ✅ Optional logging decorator for class usage.
22
+ - ✅ Clear error messages for invalid providers, missing dependencies, and circular graphs.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install syrinject
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```ts
33
+ import { Container } from "syrinject";
34
+
35
+ // Tokens can be string, symbol, or class constructors
36
+ const LoggerToken = "Logger";
37
+
38
+ interface Logger {
39
+ log(message: string): void;
40
+ }
41
+
42
+ class ConsoleLogger implements Logger {
43
+ log(message: string) {
44
+ console.log(message);
45
+ }
46
+ }
47
+
48
+ const container = new Container();
49
+
50
+ // Register a token with a class provider
51
+ container.register(LoggerToken, {
52
+ useClass: ConsoleLogger,
53
+ singleton: true,
54
+ });
55
+
56
+ const logger = container.resolve<Logger>(LoggerToken);
57
+ logger.log("Hello from Syrinject!");
58
+ ```
59
+
60
+ ## Core Concepts
61
+
62
+ ### Tokens
63
+
64
+ Tokens are identifiers for dependencies. Use a `string`, `symbol`, or a class constructor.
65
+
66
+ ```ts
67
+ export interface HttpClient {
68
+ get(url: string): Promise<string>;
69
+ }
70
+
71
+ export const HttpClientToken = "HttpClient";
72
+ ```
73
+
74
+ ### Providers
75
+
76
+ Providers define how a dependency is created.
77
+
78
+ #### Class Provider
79
+
80
+ ```ts
81
+ container.register(HttpClientToken, {
82
+ useClass: FetchHttpClient,
83
+ singleton: false,
84
+ deps: [LoggerToken],
85
+ });
86
+ ```
87
+
88
+ #### Value Provider
89
+
90
+ ```ts
91
+ container.register(HttpClientToken, {
92
+ useValue: { get: async (url) => fetch(url).then((r) => r.text()) },
93
+ });
94
+ ```
95
+
96
+ #### Factory Provider
97
+
98
+ ```ts
99
+ container.register(HttpClientToken, {
100
+ useFactory: (c) => new FetchHttpClient(c.resolve(LoggerToken)),
101
+ singleton: true,
102
+ });
103
+ ```
104
+
105
+ ### Lifetimes
106
+
107
+ - `singleton: true` creates and caches a single instance.
108
+ - `singleton: false` creates a new instance on each resolve (default).
109
+
110
+ ### Decorator: Logger
111
+
112
+ Syrinject includes a `Logger` decorator that logs instantiation and public method calls.
113
+ To show dependencies and singleton status in logs, set static `deps` (or `dependencies`) and `singleton` on the class.
114
+
115
+ ```ts
116
+ import { Container, Logger } from "syrinject";
117
+
118
+ const LoggerToken = "Logger";
119
+
120
+ interface LoggerService {
121
+ log(message: string): void;
122
+ }
123
+
124
+ class ConsoleLogger implements LoggerService {
125
+ log(message: string) {
126
+ console.log(message);
127
+ }
128
+ }
129
+
130
+ @Logger()
131
+ class UserService {
132
+ static deps = [LoggerToken];
133
+ static singleton = true;
134
+
135
+ constructor(private readonly logger: LoggerService) {}
136
+
137
+ createUser(name: string) {
138
+ this.logger.log(`Creating user: ${name}`);
139
+ }
140
+ }
141
+
142
+ const container = new Container();
143
+ container.register(LoggerToken, { useClass: ConsoleLogger, singleton: true });
144
+ container.register(UserService, { useClass: UserService, deps: UserService.deps, singleton: true });
145
+
146
+ const service = container.resolve(UserService);
147
+ service.createUser("Ada");
148
+ ```
149
+
150
+ ## Container API
151
+
152
+ ### `register(token, provider)`
153
+
154
+ Registers a provider for a token.
155
+
156
+ ### `resolve(token)`
157
+
158
+ Resolves an instance for the given token.
159
+
160
+ ### `unregister(token)`
161
+
162
+ Removes a token from the container.
163
+
164
+ ### `clear()`
165
+
166
+ Clears all registrations.
167
+
168
+ ### `has(token)`
169
+
170
+ Checks if a token is registered.
171
+
172
+ ### `registerClass(...)`
173
+
174
+ Registers a class provider. You can pass a token + class, or only a class.
175
+ When only a class is provided, the token will be the class name (string).
176
+
177
+ ### `registerValue(token, value)`
178
+
179
+ Registers a value provider.
180
+
181
+ ### `registerFactory(token, factory, options?)`
182
+
183
+ Registers a factory provider.
184
+
185
+ ## Errors
186
+
187
+ Syrinject throws descriptive errors to simplify debugging:
188
+
189
+ - `DependencyNotFoundError` when a token is not registered.
190
+ - `InvalidProviderError` when a provider is malformed.
191
+ - `CircularDependencyError` when a dependency graph loops.
192
+
193
+ ## TypeScript Configuration
194
+
195
+ If you use the `Logger` decorator, enable decorators in `tsconfig.json`:
196
+
197
+ ```json
198
+ {
199
+ "compilerOptions": {
200
+ "experimentalDecorators": true,
201
+ "emitDecoratorMetadata": false
202
+ }
203
+ }
204
+ ```
205
+
206
+ ## License
207
+
208
+ MIT
Binary file
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const { Container, CircularDependencyError, DependencyNotFoundError, InvalidProviderError } = require(node_path_1.default.resolve(__dirname, '..', '..', 'dist'));
10
+ (0, node_test_1.default)('resolve class with deps from options', () => {
11
+ class Engine {
12
+ constructor() {
13
+ this.kind = 'v8';
14
+ }
15
+ }
16
+ class Car {
17
+ constructor(engine) {
18
+ this.engine = engine;
19
+ }
20
+ }
21
+ const container = new Container();
22
+ container.register(Engine, Engine);
23
+ container.register(Car, Car, { deps: [Engine] });
24
+ const car = container.resolve(Car);
25
+ strict_1.default.equal(car.engine.kind, 'v8');
26
+ });
27
+ (0, node_test_1.default)('registerClass infers token by class name', () => {
28
+ class Service {
29
+ constructor() {
30
+ this.value = 42;
31
+ }
32
+ }
33
+ const container = new Container();
34
+ container.registerClass(Service);
35
+ const resolved = container.resolve('Service');
36
+ strict_1.default.ok(resolved instanceof Service);
37
+ strict_1.default.equal(resolved.value, 42);
38
+ });
39
+ (0, node_test_1.default)('singleton class provider returns same instance', () => {
40
+ class Counter {
41
+ constructor() {
42
+ this.count = 0;
43
+ }
44
+ }
45
+ const container = new Container();
46
+ container.register(Counter, Counter, { singleton: true });
47
+ const a = container.resolve(Counter);
48
+ const b = container.resolve(Counter);
49
+ strict_1.default.equal(a, b);
50
+ });
51
+ (0, node_test_1.default)('value provider returns same value', () => {
52
+ const container = new Container();
53
+ container.registerValue('config', { port: 3000 });
54
+ const config = container.resolve('config');
55
+ strict_1.default.deepEqual(config, { port: 3000 });
56
+ });
57
+ (0, node_test_1.default)('factory provider builds instance', () => {
58
+ const container = new Container();
59
+ container.registerFactory('random', () => ({ value: Math.random() }));
60
+ const a = container.resolve('random');
61
+ const b = container.resolve('random');
62
+ strict_1.default.notEqual(a, b);
63
+ strict_1.default.equal(typeof a.value, 'number');
64
+ });
65
+ (0, node_test_1.default)('factory provider respects singleton', () => {
66
+ const container = new Container();
67
+ container.registerFactory('singleton', () => ({ value: Math.random() }), { singleton: true });
68
+ const a = container.resolve('singleton');
69
+ const b = container.resolve('singleton');
70
+ strict_1.default.equal(a, b);
71
+ });
72
+ (0, node_test_1.default)('throws DependencyNotFoundError for missing token', () => {
73
+ const container = new Container();
74
+ strict_1.default.throws(() => container.resolve('missing'), (err) => err instanceof DependencyNotFoundError);
75
+ });
76
+ (0, node_test_1.default)('throws CircularDependencyError when cycle exists', () => {
77
+ class A {
78
+ }
79
+ class B {
80
+ }
81
+ const container = new Container();
82
+ container.register(A, A, { deps: [B] });
83
+ container.register(B, B, { deps: [A] });
84
+ strict_1.default.throws(() => container.resolve(A), (err) => err instanceof CircularDependencyError);
85
+ });
86
+ (0, node_test_1.default)('throws InvalidProviderError for invalid provider', () => {
87
+ const container = new Container();
88
+ container.register('invalid', {});
89
+ strict_1.default.throws(() => container.resolve('invalid'), (err) => err instanceof InvalidProviderError);
90
+ });
91
+ (0, node_test_1.default)('has, unregister, clear work as expected', () => {
92
+ const container = new Container();
93
+ container.registerValue('token', 123);
94
+ strict_1.default.equal(container.has('token'), true);
95
+ container.unregister('token');
96
+ strict_1.default.equal(container.has('token'), false);
97
+ container.registerValue('a', 1);
98
+ container.registerValue('b', 2);
99
+ container.clear();
100
+ strict_1.default.equal(container.has('a'), false);
101
+ strict_1.default.equal(container.has('b'), false);
102
+ });
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const { Container, CircularDependencyError, DependencyNotFoundError, InvalidProviderError } = require(node_path_1.default.resolve(__dirname, '..', '..', 'dist'));
10
+ (0, node_test_1.default)('resolve class with deps from options', () => {
11
+ class Engine {
12
+ constructor() {
13
+ this.kind = 'v8';
14
+ }
15
+ }
16
+ class Car {
17
+ constructor(engine) {
18
+ this.engine = engine;
19
+ }
20
+ }
21
+ const container = new Container();
22
+ container.register(Engine, Engine);
23
+ container.register(Car, Car, { deps: [Engine] });
24
+ const car = container.resolve(Car);
25
+ strict_1.default.equal(car.engine.kind, 'v8');
26
+ });
27
+ (0, node_test_1.default)('registerClass infers token by class name', () => {
28
+ class Service {
29
+ constructor() {
30
+ this.value = 42;
31
+ }
32
+ }
33
+ const container = new Container();
34
+ container.registerClass(Service);
35
+ const resolved = container.resolve('Service');
36
+ strict_1.default.ok(resolved instanceof Service);
37
+ strict_1.default.equal(resolved.value, 42);
38
+ });
39
+ (0, node_test_1.default)('singleton class provider returns same instance', () => {
40
+ class Counter {
41
+ constructor() {
42
+ this.count = 0;
43
+ }
44
+ }
45
+ const container = new Container();
46
+ container.register(Counter, Counter, { singleton: true });
47
+ const a = container.resolve(Counter);
48
+ const b = container.resolve(Counter);
49
+ strict_1.default.equal(a, b);
50
+ });
51
+ (0, node_test_1.default)('value provider returns same value', () => {
52
+ const container = new Container();
53
+ container.registerValue('config', { port: 3000 });
54
+ const config = container.resolve('config');
55
+ strict_1.default.deepEqual(config, { port: 3000 });
56
+ });
57
+ (0, node_test_1.default)('factory provider builds instance', () => {
58
+ const container = new Container();
59
+ container.registerFactory('random', () => ({ value: Math.random() }));
60
+ const a = container.resolve('random');
61
+ const b = container.resolve('random');
62
+ strict_1.default.notEqual(a, b);
63
+ strict_1.default.equal(typeof a.value, 'number');
64
+ });
65
+ (0, node_test_1.default)('factory provider respects singleton', () => {
66
+ const container = new Container();
67
+ container.registerFactory('singleton', () => ({ value: Math.random() }), { singleton: true });
68
+ const a = container.resolve('singleton');
69
+ const b = container.resolve('singleton');
70
+ strict_1.default.equal(a, b);
71
+ });
72
+ (0, node_test_1.default)('throws DependencyNotFoundError for missing token', () => {
73
+ const container = new Container();
74
+ strict_1.default.throws(() => container.resolve('missing'), (err) => err instanceof DependencyNotFoundError);
75
+ });
76
+ (0, node_test_1.default)('throws CircularDependencyError when cycle exists', () => {
77
+ class A {
78
+ }
79
+ class B {
80
+ }
81
+ const container = new Container();
82
+ container.register(A, A, { deps: [B] });
83
+ container.register(B, B, { deps: [A] });
84
+ strict_1.default.throws(() => container.resolve(A), (err) => err instanceof CircularDependencyError);
85
+ });
86
+ (0, node_test_1.default)('throws InvalidProviderError for invalid provider', () => {
87
+ const container = new Container();
88
+ container.register('invalid', {});
89
+ strict_1.default.throws(() => container.resolve('invalid'), (err) => err instanceof InvalidProviderError);
90
+ });
91
+ (0, node_test_1.default)('has, unregister, clear work as expected', () => {
92
+ const container = new Container();
93
+ container.registerValue('token', 123);
94
+ strict_1.default.equal(container.has('token'), true);
95
+ container.unregister('token');
96
+ strict_1.default.equal(container.has('token'), false);
97
+ container.registerValue('a', 1);
98
+ container.registerValue('b', 2);
99
+ container.clear();
100
+ strict_1.default.equal(container.has('a'), false);
101
+ strict_1.default.equal(container.has('b'), false);
102
+ });
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const dist_1 = require("../dist");
9
+ (0, node_test_1.default)('resolve class with deps from options', () => {
10
+ class Engine {
11
+ constructor() {
12
+ this.kind = 'v8';
13
+ }
14
+ }
15
+ class Car {
16
+ constructor(engine) {
17
+ this.engine = engine;
18
+ }
19
+ }
20
+ const container = new dist_1.Container();
21
+ container.register(Engine, Engine);
22
+ container.register(Car, Car, { deps: [Engine] });
23
+ const car = container.resolve(Car);
24
+ strict_1.default.equal(car.engine.kind, 'v8');
25
+ });
26
+ (0, node_test_1.default)('registerClass infers token by class name', () => {
27
+ class Service {
28
+ constructor() {
29
+ this.value = 42;
30
+ }
31
+ }
32
+ const container = new dist_1.Container();
33
+ container.registerClass(Service);
34
+ const resolved = container.resolve('Service');
35
+ strict_1.default.ok(resolved instanceof Service);
36
+ strict_1.default.equal(resolved.value, 42);
37
+ });
38
+ (0, node_test_1.default)('singleton class provider returns same instance', () => {
39
+ class Counter {
40
+ constructor() {
41
+ this.count = 0;
42
+ }
43
+ }
44
+ const container = new dist_1.Container();
45
+ container.register(Counter, Counter, { singleton: true });
46
+ const a = container.resolve(Counter);
47
+ const b = container.resolve(Counter);
48
+ strict_1.default.equal(a, b);
49
+ });
50
+ (0, node_test_1.default)('value provider returns same value', () => {
51
+ const container = new dist_1.Container();
52
+ container.registerValue('config', { port: 3000 });
53
+ const config = container.resolve('config');
54
+ strict_1.default.deepEqual(config, { port: 3000 });
55
+ });
56
+ (0, node_test_1.default)('factory provider builds instance', () => {
57
+ const container = new dist_1.Container();
58
+ container.registerFactory('random', () => ({ value: Math.random() }));
59
+ const a = container.resolve('random');
60
+ const b = container.resolve('random');
61
+ strict_1.default.notEqual(a, b);
62
+ strict_1.default.equal(typeof a.value, 'number');
63
+ });
64
+ (0, node_test_1.default)('factory provider respects singleton', () => {
65
+ const container = new dist_1.Container();
66
+ container.registerFactory('singleton', () => ({ value: Math.random() }), { singleton: true });
67
+ const a = container.resolve('singleton');
68
+ const b = container.resolve('singleton');
69
+ strict_1.default.equal(a, b);
70
+ });
71
+ (0, node_test_1.default)('throws DependencyNotFoundError for missing token', () => {
72
+ const container = new dist_1.Container();
73
+ strict_1.default.throws(() => container.resolve('missing'), (err) => err instanceof dist_1.DependencyNotFoundError);
74
+ });
75
+ (0, node_test_1.default)('throws CircularDependencyError when cycle exists', () => {
76
+ class A {
77
+ }
78
+ class B {
79
+ }
80
+ const container = new dist_1.Container();
81
+ container.register(A, A, { deps: [B] });
82
+ container.register(B, B, { deps: [A] });
83
+ strict_1.default.throws(() => container.resolve(A), (err) => err instanceof dist_1.CircularDependencyError);
84
+ });
85
+ (0, node_test_1.default)('throws InvalidProviderError for invalid provider', () => {
86
+ const container = new dist_1.Container();
87
+ container.register('invalid', {});
88
+ strict_1.default.throws(() => container.resolve('invalid'), (err) => err instanceof dist_1.InvalidProviderError);
89
+ });
90
+ (0, node_test_1.default)('has, unregister, clear work as expected', () => {
91
+ const container = new dist_1.Container();
92
+ container.registerValue('token', 123);
93
+ strict_1.default.equal(container.has('token'), true);
94
+ container.unregister('token');
95
+ strict_1.default.equal(container.has('token'), false);
96
+ container.registerValue('a', 1);
97
+ container.registerValue('b', 2);
98
+ container.clear();
99
+ strict_1.default.equal(container.has('a'), false);
100
+ strict_1.default.equal(container.has('b'), false);
101
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "syrinject",
3
3
  "description": "Simple dependency injection container",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
@@ -11,13 +11,15 @@
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.build.json",
13
13
  "dev": "tsc -w",
14
- "prepublishOnly": "npm run build"
14
+ "prepublishOnly": "npm run build",
15
+ "test": "npm run build && tsc -p tsconfig.test.json && node --test \"dist/test/*.test.js\""
15
16
  },
16
17
  "keywords": [],
17
18
  "author": "andersonreges",
18
19
  "license": "ISC",
19
20
  "type": "commonjs",
20
21
  "devDependencies": {
22
+ "@types/node": "^25.2.0",
21
23
  "typescript": "^5.9.3"
22
24
  }
23
25
  }
package/dist/package.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "name": "simple-di-container",
3
- "description": "Simple dependency injection container",
4
- "version": "0.1.0",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": ["dist"],
8
- "scripts": {
9
- "build": "tsc -p tsconfig.build.json",
10
- "dev": "tsc -w",
11
- "prepublishOnly": "npm run build"
12
- },
13
- "keywords": [],
14
- "author": "andersonreges",
15
- "license": "ISC",
16
- "type": "commonjs",
17
- "devDependencies": {
18
- "typescript": "^5.9.3"
19
- }
20
- }