injectkit 1.0.0

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 ADDED
@@ -0,0 +1,457 @@
1
+ # InjectKit
2
+
3
+ [![codecov](https://codecov.io/github/MaroonedSoftware/InjectKit/graph/badge.svg?token=suXBzveqVf)](https://codecov.io/github/MaroonedSoftware/InjectKit)
4
+
5
+ ---
6
+
7
+ <p align="center">
8
+ <strong>A lightweight, type-safe dependency injection container for TypeScript</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="#features">Features</a> •
13
+ <a href="#installation">Installation</a> •
14
+ <a href="#quick-start">Quick Start</a> •
15
+ <a href="#core-concepts">Core Concepts</a> •
16
+ <a href="#api-reference">API Reference</a> •
17
+ <a href="#license">License</a>
18
+ </p>
19
+
20
+ ---
21
+
22
+ ## Features
23
+
24
+ - 🎯 **Type-safe** — Full TypeScript support with strong typing throughout
25
+ - 🪶 **Lightweight** — Minimal footprint with zero dependencies (except `reflect-metadata`)
26
+ - 🔄 **Multiple lifetimes** — Singleton, transient, and scoped instance management
27
+ - 🏭 **Flexible registration** — Classes, factories, or existing instances
28
+ - 📦 **Collection support** — Register arrays and maps of implementations
29
+ - 🔍 **Validation** — Automatic detection of missing and circular dependencies
30
+ - 🧪 **Test-friendly** — Easy mocking with scoped container overrides
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install injectkit reflect-metadata
36
+ ```
37
+
38
+ ```bash
39
+ pnpm add injectkit reflect-metadata
40
+ ```
41
+
42
+ ```bash
43
+ yarn add injectkit reflect-metadata
44
+ ```
45
+
46
+ ## Requirements
47
+
48
+ - **Node.js** >= 20
49
+ - **TypeScript** with the following compiler options enabled:
50
+
51
+ ```json
52
+ {
53
+ "compilerOptions": {
54
+ "experimentalDecorators": true,
55
+ "emitDecoratorMetadata": true
56
+ }
57
+ }
58
+ ```
59
+
60
+ - Import `reflect-metadata` **once** at your application entry point:
61
+
62
+ ```typescript
63
+ import 'reflect-metadata';
64
+ ```
65
+
66
+ ## Quick Start
67
+
68
+ ```typescript
69
+ import 'reflect-metadata';
70
+ import { Injectable, InjectKitRegistry, Container } from 'injectkit';
71
+
72
+ // 1. Decorate your classes with @Injectable()
73
+ @Injectable()
74
+ class Logger {
75
+ log(message: string) {
76
+ console.log(`[LOG] ${message}`);
77
+ }
78
+ }
79
+
80
+ @Injectable()
81
+ class UserService {
82
+ constructor(private logger: Logger) {}
83
+
84
+ createUser(name: string) {
85
+ this.logger.log(`Creating user: ${name}`);
86
+ return { id: crypto.randomUUID(), name };
87
+ }
88
+ }
89
+
90
+ // 2. Create a registry and register your services
91
+ const registry = new InjectKitRegistry();
92
+ registry.register(Logger).useClass(Logger).asSingleton();
93
+ registry.register(UserService).useClass(UserService).asSingleton();
94
+
95
+ // 3. Build the container
96
+ const container = registry.build();
97
+
98
+ // 4. Resolve and use your services
99
+ const userService = container.get(UserService);
100
+ userService.createUser('Alice');
101
+ ```
102
+
103
+ ## Core Concepts
104
+
105
+ ### Registry
106
+
107
+ The **Registry** is where you configure your services before runtime. It validates all registrations when building the container.
108
+
109
+ ```typescript
110
+ const registry = new InjectKitRegistry();
111
+
112
+ // Register services
113
+ registry.register(MyService).useClass(MyService).asSingleton();
114
+
115
+ // Check if registered
116
+ registry.isRegistered(MyService); // true
117
+
118
+ // Remove if needed
119
+ registry.remove(MyService);
120
+
121
+ // Build the container when ready
122
+ const container = registry.build();
123
+ ```
124
+
125
+ ### Container
126
+
127
+ The **Container** resolves and manages service instances at runtime. It automatically injects dependencies declared in constructors.
128
+
129
+ ```typescript
130
+ // Resolve a service (dependencies are injected automatically)
131
+ const service = container.get(MyService);
132
+
133
+ // The Container itself can be resolved for factory patterns
134
+ const container = container.get(Container);
135
+ ```
136
+
137
+ ### Identifier
138
+
139
+ An **Identifier** is a class constructor or abstract class used to register and resolve services. This enables programming to interfaces:
140
+
141
+ ```typescript
142
+ // Abstract class as identifier
143
+ abstract class Repository {
144
+ abstract find(id: string): Promise<Entity>;
145
+ }
146
+
147
+ // Concrete implementation
148
+ @Injectable()
149
+ class PostgresRepository extends Repository {
150
+ async find(id: string) {
151
+ /* ... */
152
+ }
153
+ }
154
+
155
+ // Register abstract → concrete mapping
156
+ registry.register(Repository).useClass(PostgresRepository).asSingleton();
157
+
158
+ // Resolve using the abstract class
159
+ const repo = container.get(Repository); // Returns PostgresRepository
160
+ ```
161
+
162
+ ### Lifetimes
163
+
164
+ InjectKit supports three lifetime strategies:
165
+
166
+ | Lifetime | Behavior |
167
+ | ------------- | ------------------------------------------------- |
168
+ | **Singleton** | One instance shared across the entire application |
169
+ | **Transient** | New instance created on every `get()` call |
170
+ | **Scoped** | One instance per scope, shared within that scope |
171
+
172
+ ```typescript
173
+ registry.register(ConfigService).useClass(ConfigService).asSingleton();
174
+ registry.register(RequestId).useClass(RequestId).asScoped();
175
+ registry.register(TempCalculation).useClass(TempCalculation).asTransient();
176
+ ```
177
+
178
+ ## API Reference
179
+
180
+ ### Registration Methods
181
+
182
+ #### `useClass(constructor)`
183
+
184
+ Register a service using its constructor. Dependencies are automatically resolved from constructor parameters.
185
+
186
+ ```typescript
187
+ @Injectable()
188
+ class EmailService {
189
+ constructor(
190
+ private config: ConfigService,
191
+ private logger: Logger,
192
+ ) {}
193
+ }
194
+
195
+ registry.register(EmailService).useClass(EmailService).asSingleton();
196
+ ```
197
+
198
+ #### `useFactory(factory)`
199
+
200
+ Register a service using a factory function. Useful for complex initialization or third-party libraries.
201
+
202
+ ```typescript
203
+ registry
204
+ .register(DatabaseConnection)
205
+ .useFactory(container => {
206
+ const config = container.get(ConfigService);
207
+ return new DatabaseConnection({
208
+ host: config.dbHost,
209
+ port: config.dbPort,
210
+ });
211
+ })
212
+ .asSingleton();
213
+ ```
214
+
215
+ #### `useInstance(instance)`
216
+
217
+ Register an existing instance directly. Always behaves as a singleton.
218
+
219
+ ```typescript
220
+ const config = new ConfigService({ env: 'production' });
221
+ registry.register(ConfigService).useInstance(config);
222
+ ```
223
+
224
+ #### `useArray(constructor)`
225
+
226
+ Register a collection of implementations. Useful for plugin systems or strategy patterns.
227
+
228
+ ```typescript
229
+ // Handler implementations
230
+ @Injectable()
231
+ class JsonHandler extends Handler {
232
+ /* ... */
233
+ }
234
+
235
+ @Injectable()
236
+ class XmlHandler extends Handler {
237
+ /* ... */
238
+ }
239
+
240
+ // Array container
241
+ @Injectable()
242
+ class Handlers extends Array<Handler> {}
243
+
244
+ // Registration
245
+ registry.register(JsonHandler).useClass(JsonHandler).asSingleton();
246
+ registry.register(XmlHandler).useClass(XmlHandler).asSingleton();
247
+ registry.register(Handlers).useArray(Handlers).push(JsonHandler).push(XmlHandler);
248
+
249
+ // Usage
250
+ const handlers = container.get(Handlers);
251
+ handlers.forEach(h => h.handle(data));
252
+ ```
253
+
254
+ #### `useMap(constructor)`
255
+
256
+ Register a keyed collection of implementations.
257
+
258
+ ```typescript
259
+ @Injectable()
260
+ class ProcessorMap extends Map<string, Processor> {}
261
+
262
+ registry.register(ProcessorMap).useMap(ProcessorMap).set('fast', FastProcessor).set('accurate', AccurateProcessor);
263
+
264
+ // Usage
265
+ const processors = container.get(ProcessorMap);
266
+ const processor = processors.get('fast');
267
+ ```
268
+
269
+ ### Container Methods
270
+
271
+ #### `get<T>(identifier): T`
272
+
273
+ Resolves an instance of the specified type.
274
+
275
+ ```typescript
276
+ const service = container.get(MyService);
277
+ ```
278
+
279
+ #### `createScopedContainer(): ScopedContainer`
280
+
281
+ Creates a child container for scoped instance management.
282
+
283
+ ```typescript
284
+ const requestScope = container.createScopedContainer();
285
+ const requestService = requestScope.get(RequestScopedService);
286
+ ```
287
+
288
+ #### `override<T>(identifier, instance): void`
289
+
290
+ Overrides a registration within a scoped container. Perfect for testing.
291
+
292
+ ```typescript
293
+ const testScope = container.createScopedContainer();
294
+
295
+ // Override with a mock
296
+ testScope.override(EmailService, {
297
+ send: vi.fn().mockResolvedValue(true),
298
+ } as EmailService);
299
+
300
+ // Tests use the mock
301
+ const service = testScope.get(NotificationService);
302
+ ```
303
+
304
+ ### Registry Methods
305
+
306
+ #### `register<T>(identifier): RegistrationType<T>`
307
+
308
+ Starts a registration chain for a service.
309
+
310
+ #### `remove<T>(identifier): void`
311
+
312
+ Removes a registration from the registry.
313
+
314
+ #### `isRegistered<T>(identifier): boolean`
315
+
316
+ Checks if a service is already registered.
317
+
318
+ #### `build(): Container`
319
+
320
+ Builds the container, validating all registrations.
321
+
322
+ ## Scoped Containers
323
+
324
+ Scoped containers enable request-scoped or unit-of-work patterns:
325
+
326
+ ```typescript
327
+ @Injectable()
328
+ class RequestContext {
329
+ readonly requestId = crypto.randomUUID();
330
+ readonly startTime = Date.now();
331
+ }
332
+
333
+ registry.register(RequestContext).useClass(RequestContext).asScoped();
334
+
335
+ // Per-request handling
336
+ app.use((req, res, next) => {
337
+ const scope = container.createScopedContainer();
338
+
339
+ // Same RequestContext instance throughout this request
340
+ const ctx = scope.get(RequestContext);
341
+ req.scope = scope;
342
+
343
+ next();
344
+ });
345
+ ```
346
+
347
+ ### Scope Hierarchy
348
+
349
+ Scoped containers inherit instances from parent scopes:
350
+
351
+ ```typescript
352
+ const root = registry.build();
353
+ const scope1 = root.createScopedContainer();
354
+ const scope2 = scope1.createScopedContainer();
355
+
356
+ // Instance created in scope1 is visible in scope2
357
+ const instance1 = scope1.get(ScopedService);
358
+ const instance2 = scope2.get(ScopedService);
359
+ console.log(instance1 === instance2); // true
360
+ ```
361
+
362
+ ## Validation
363
+
364
+ InjectKit validates your dependency graph when calling `build()`:
365
+
366
+ ### Missing Dependencies
367
+
368
+ ```typescript
369
+ @Injectable()
370
+ class UserService {
371
+ constructor(private db: DatabaseService) {} // Not registered!
372
+ }
373
+
374
+ registry.register(UserService).useClass(UserService).asSingleton();
375
+ registry.build(); // ❌ Error: Missing dependencies for UserService: DatabaseService
376
+ ```
377
+
378
+ ### Circular Dependencies
379
+
380
+ ```typescript
381
+ @Injectable()
382
+ class ServiceA {
383
+ constructor(private b: ServiceB) {}
384
+ }
385
+
386
+ @Injectable()
387
+ class ServiceB {
388
+ constructor(private a: ServiceA) {}
389
+ }
390
+
391
+ registry.register(ServiceA).useClass(ServiceA).asSingleton();
392
+ registry.register(ServiceB).useClass(ServiceB).asSingleton();
393
+ registry.build(); // ❌ Error: Circular dependency found: ServiceA -> ServiceB -> ServiceA
394
+ ```
395
+
396
+ ### Missing Decorator
397
+
398
+ ```typescript
399
+ class ForgotDecorator {
400
+ constructor(private dep: SomeDependency) {}
401
+ }
402
+
403
+ registry.register(ForgotDecorator).useClass(ForgotDecorator).asSingleton();
404
+ registry.build(); // ❌ Error: Service not decorated: ForgotDecorator
405
+ ```
406
+
407
+ ## Testing
408
+
409
+ InjectKit makes testing easy with scoped overrides:
410
+
411
+ ```typescript
412
+ import { describe, it, expect, beforeEach } from 'vitest';
413
+
414
+ describe('UserService', () => {
415
+ let scope: ScopedContainer;
416
+ let mockDb: DatabaseService;
417
+
418
+ beforeEach(() => {
419
+ scope = container.createScopedContainer();
420
+
421
+ mockDb = {
422
+ query: vi.fn().mockResolvedValue([{ id: '1', name: 'Test' }]),
423
+ } as unknown as DatabaseService;
424
+
425
+ scope.override(DatabaseService, mockDb);
426
+ });
427
+
428
+ it('should fetch users', async () => {
429
+ const userService = scope.get(UserService);
430
+ const users = await userService.getUsers();
431
+
432
+ expect(users).toHaveLength(1);
433
+ expect(mockDb.query).toHaveBeenCalled();
434
+ });
435
+ });
436
+ ```
437
+
438
+ ## TypeScript Configuration
439
+
440
+ Recommended `tsconfig.json` settings:
441
+
442
+ ```json
443
+ {
444
+ "compilerOptions": {
445
+ "target": "ES2022",
446
+ "module": "ESNext",
447
+ "moduleResolution": "bundler",
448
+ "experimentalDecorators": true,
449
+ "emitDecoratorMetadata": true,
450
+ "strict": true
451
+ }
452
+ }
453
+ ```
454
+
455
+ ## License
456
+
457
+ MIT
@@ -0,0 +1,63 @@
1
+ import { Container, Identifier, Instance, ScopedContainer } from './interfaces.js';
2
+ import { Registration } from './internal.js';
3
+ /**
4
+ * Implementation of the dependency injection container.
5
+ * Manages service registrations and resolves instances based on their lifetime strategy.
6
+ */
7
+ export declare class InjectKitContainer implements ScopedContainer, Container {
8
+ private readonly registrations;
9
+ private readonly parent?;
10
+ /** Map storing cached instances for singleton and scoped lifetimes. */
11
+ private readonly instances;
12
+ /**
13
+ * Creates a new container instance.
14
+ * @param registrations Map of registered services and their configurations.
15
+ * @param parent Optional parent container for scoped container hierarchies.
16
+ */
17
+ constructor(registrations: Map<Identifier<unknown>, Registration<unknown>>, parent?: InjectKitContainer | undefined);
18
+ /**
19
+ * Creates a new instance of the specified type based on its registration configuration.
20
+ * Handles constructor-based, factory-based, and instance-based registrations.
21
+ * Manages singleton and scoped instance caching.
22
+ * Also handles array and map collection dependencies by populating them with resolved instances.
23
+ * @template T The type of instance to create.
24
+ * @param id The identifier for the type to instantiate.
25
+ * @param registration The registration configuration containing creation strategy.
26
+ * @returns A new or cached instance of type T.
27
+ * @throws {Error} If the registration is invalid (no constructor, factory, or instance provided).
28
+ */
29
+ private createInstance;
30
+ /**
31
+ * Retrieves a cached scoped instance by traversing up the container hierarchy.
32
+ * For scoped lifetimes, instances are stored in the container where they were created.
33
+ * @template T The type of instance to retrieve.
34
+ * @param id The identifier for the type to retrieve.
35
+ * @returns The cached scoped instance, or undefined if not found.
36
+ */
37
+ private getScopedInstance;
38
+ /**
39
+ * Retrieves an instance of the specified type from the container.
40
+ * For singleton and scoped lifetimes, returns cached instances when available.
41
+ * For transient lifetimes, creates a new instance each time.
42
+ * @template T The type of instance to retrieve.
43
+ * @param id The identifier (constructor or abstract class) for the type to resolve.
44
+ * @returns An instance of type T.
45
+ * @throws {Error} If no registration is found for the specified identifier.
46
+ */
47
+ get<T>(id: Identifier<T>): T;
48
+ /**
49
+ * Creates a new scoped container that inherits all registrations from this container.
50
+ * Scoped containers allow for per-scope instance management, where scoped services
51
+ * are shared within a scope but isolated between different scopes.
52
+ * @returns A new scoped container instance with this container as its parent.
53
+ */
54
+ createScopedContainer(): ScopedContainer;
55
+ /**
56
+ * Overrides the instance of the specified type in the container.
57
+ * @template T The type of instance to override.
58
+ * @param id The identifier for the type to override.
59
+ * @param instance The instance to override.
60
+ */
61
+ override<T>(id: Identifier<T>, instance: Instance<T>): void;
62
+ }
63
+ //# sourceMappingURL=container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,eAAe,EAAE,SAAS;IAUjE,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAV1B,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2C;IAErE;;;;OAIG;gBAEgB,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,EAC9D,MAAM,CAAC,EAAE,kBAAkB,YAAA;IAG9C;;;;;;;;;;OAUG;IACH,OAAO,CAAC,cAAc;IA8CtB;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAUzB;;;;;;;;OAQG;IACI,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC;IAgBnC;;;;;OAKG;IACI,qBAAqB,IAAI,eAAe;IAI/C;;;;;OAKG;IACI,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI;CAYnE"}
@@ -0,0 +1,4 @@
1
+ export * from './injectable.js';
2
+ export * from './interfaces.js';
3
+ export * from './registry.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC"}