@djodjonx/wiredi 0.0.10 → 0.0.12

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.
Files changed (35) hide show
  1. package/README.md +147 -20
  2. package/dist/index.cjs +12 -4
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +70 -17
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.mts +70 -17
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +12 -4
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/plugin/ConfigurationAnalyzer.d.ts +59 -0
  11. package/dist/plugin/ConfigurationAnalyzer.d.ts.map +1 -0
  12. package/dist/plugin/ConfigurationAnalyzer.js +321 -0
  13. package/dist/plugin/ConfigurationAnalyzer.js.map +1 -0
  14. package/dist/plugin/DependencyAnalyzer.d.ts +54 -0
  15. package/dist/plugin/DependencyAnalyzer.d.ts.map +1 -0
  16. package/dist/plugin/DependencyAnalyzer.js +208 -0
  17. package/dist/plugin/DependencyAnalyzer.js.map +1 -0
  18. package/dist/plugin/TokenNormalizer.d.ts +51 -0
  19. package/dist/plugin/TokenNormalizer.d.ts.map +1 -0
  20. package/dist/plugin/TokenNormalizer.js +208 -0
  21. package/dist/plugin/TokenNormalizer.js.map +1 -0
  22. package/dist/plugin/ValidationEngine.d.ts +53 -0
  23. package/dist/plugin/ValidationEngine.d.ts.map +1 -0
  24. package/dist/plugin/ValidationEngine.js +250 -0
  25. package/dist/plugin/ValidationEngine.js.map +1 -0
  26. package/dist/plugin/index.d.ts +2 -0
  27. package/dist/plugin/index.d.ts.map +1 -0
  28. package/dist/plugin/index.js +144 -0
  29. package/dist/plugin/index.js.map +1 -0
  30. package/dist/plugin/package.json +6 -0
  31. package/dist/plugin/types.d.ts +152 -0
  32. package/dist/plugin/types.d.ts.map +1 -0
  33. package/dist/plugin/types.js +3 -0
  34. package/dist/plugin/types.js.map +1 -0
  35. package/package.json +9 -1
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <h1 align="center">Wire your dependency injection with type safety</h1>
14
14
 
15
15
  <p align="center">
16
- <strong>WireDI</strong> is an abstraction layer on top of popular DI containers (tsyringe, Awilix, InversifyJS) that wires your dependencies with compile-time validation.
16
+ <strong>WireDI</strong> is a declarative, type-safe Dependency Injection builder. It eliminates "autowire" magic in favor of explicit, compile-time validated definitions, ensuring your application is free from missing dependencies and type mismatches.
17
17
  </p>
18
18
 
19
19
  ---
@@ -33,9 +33,24 @@
33
33
 
34
34
  ## Why WireDI?
35
35
 
36
- Dependency injection containers like tsyringe or InversifyJS are powerful, but they fail **at runtime** when a dependency is missing or misconfigured.
36
+ Traditional DI containers (like tsyringe, InversifyJS) are powerful but often rely on runtime "magic" (autowiring) that can lead to:
37
+ - 💥 **Runtime errors** when dependencies are missing.
38
+ - 🐛 **Silent failures** when incorrect types are injected.
37
39
 
38
- **With WireDI**, these errors are detected **in your IDE** before you even run the code:
40
+ **WireDI** solves this by shifting validation to **compile-time**:
41
+
42
+ ### 1. Declarative & Explicit
43
+ WireDI ignores autowiring in favor of **explicit declarations**. You define exactly what is injected. This ensures you never have a "missing dependency" error in production and your dependency graph is transparent.
44
+
45
+ ### 2. Smart & Safe Registration
46
+ - **Singleton by Default**: All dependencies are registered as singletons, ensuring state consistency across your application.
47
+ - **Idempotent Builder**: The `useBuilder` system prevents double registration. If multiple builders try to register the same token in the same container, WireDI respects the existing one. This allows you to safely compose overlapping modules without conflicts.
48
+
49
+ ### 3. Modular "Extend" Pattern
50
+ Build your app like Lego. Create **partial configurations** for specific domains (e.g., Auth, Database, Logging) and **extend** them in your main application builder. This separation of concerns makes your config maintainable and testable.
51
+
52
+ ### 4. Real-Time Validation
53
+ Errors are detected **in your IDE** instantly:
39
54
 
40
55
  ```typescript
41
56
  // ❌ Error detected in IDE: "Logger" is not registered
@@ -44,7 +59,7 @@ const config = defineBuilderConfig({
44
59
  injections: [
45
60
  { token: UserService }, // UserService depends on Logger
46
61
  ],
47
- listeners: [],
62
+ // listeners is optional
48
63
  })
49
64
  ```
50
65
 
@@ -57,7 +72,7 @@ Unlike traditional DI containers, **WireDI's type checking works without decorat
57
72
  - ✅ No need for `@injectable` or `@inject` decorators
58
73
  - ✅ Framework-agnostic type safety
59
74
 
60
- **Learn more**: [Type Checking Without Decorators](docs/Agent/TYPE_CHECKING_WITHOUT_DECORATORS.md)
75
+ **Learn more**: [Type Checking Without Decorators](docs/TYPE_CHECKING_WITHOUT_DECORATORS.md)
61
76
 
62
77
  ## Installation
63
78
 
@@ -195,7 +210,7 @@ const loggingPartial = definePartialConfig({
195
210
  injections: [
196
211
  { token: TOKENS.Logger, provider: ConsoleLogger },
197
212
  ],
198
- listeners: [],
213
+ // listeners is optional - omit if you don't need event handling
199
214
  })
200
215
 
201
216
  // Main configuration
@@ -206,9 +221,10 @@ export const appConfig = defineBuilderConfig({
206
221
  { token: TOKENS.UserRepository, provider: UserRepository },
207
222
  { token: UserService }, // Class used as token
208
223
  ],
209
- listeners: [],
224
+ // listeners is optional - only add if you need event handling
210
225
  })
211
226
  ```
227
+ ```
212
228
 
213
229
  ### 4. Use the builder
214
230
 
@@ -259,11 +275,13 @@ The TypeScript Language Service plugin detects configuration errors **directly i
259
275
 
260
276
  ### Detected Errors
261
277
 
262
- | Error Type | Description |
263
- |------------|-------------|
264
- | 🔴 Missing dependency | A service requires an unregistered token |
265
- | 🔴 Type mismatch | The provider doesn't implement the expected interface |
266
- | 🔴 Unregistered @inject token | A decorator references a missing token |
278
+ | Error Type | Description | Error Message |
279
+ |------------|-------------|---------------|
280
+ | 🔴 Missing dependency | A service requires an unregistered token | `[WireDI] Missing dependency: ...` |
281
+ | 🔴 Type mismatch | The provider doesn't implement the expected interface | `[WireDI] Type incompatible: ...` |
282
+ | 🔴 Token collision | Token already registered in a partial | `[WireDI] This token is already registered in a partial` |
283
+ | 🔴 Duplicate listener | Same (event, listener) pair registered twice | `[WireDI] Duplicate listener in the same configuration` |
284
+ | 🔴 Listener collision | Listener already registered in a partial | `[WireDI] This event listener is already registered in a partial` |
267
285
 
268
286
  ### Error Example
269
287
 
@@ -274,7 +292,7 @@ const config = defineBuilderConfig({
274
292
  injections: [
275
293
  { token: TOKENS.UserRepository, provider: ConsoleLogger }, // Error here!
276
294
  ],
277
- listeners: [],
295
+ // listeners is optional
278
296
  })
279
297
  ```
280
298
 
@@ -348,7 +366,6 @@ export const loggingPartial = definePartialConfig({
348
366
  injections: [
349
367
  { token: TOKENS.Logger, provider: ConsoleLogger },
350
368
  ],
351
- listeners: [],
352
369
  })
353
370
 
354
371
  // partials/repositories.ts
@@ -357,7 +374,6 @@ export const repositoriesPartial = definePartialConfig({
357
374
  { token: TOKENS.UserRepository, provider: PostgresUserRepository },
358
375
  { token: TOKENS.ProductRepository, provider: PostgresProductRepository },
359
376
  ],
360
- listeners: [],
361
377
  })
362
378
 
363
379
  // config.ts
@@ -368,7 +384,6 @@ export const appConfig = defineBuilderConfig({
368
384
  { token: UserService },
369
385
  { token: ProductService },
370
386
  ],
371
- listeners: [],
372
387
  })
373
388
  ```
374
389
 
@@ -382,7 +397,6 @@ const loggingPartial = definePartialConfig({
382
397
  injections: [
383
398
  { token: TOKENS.Logger, provider: ConsoleLogger }
384
399
  ],
385
- listeners: []
386
400
  })
387
401
 
388
402
  export const appConfig = defineBuilderConfig({
@@ -392,7 +406,6 @@ export const appConfig = defineBuilderConfig({
392
406
  // ❌ This will cause a TypeScript error - token already defined in partial
393
407
  { token: TOKENS.Logger, provider: FileLogger }
394
408
  ],
395
- listeners: []
396
409
  })
397
410
  ```
398
411
 
@@ -407,10 +420,125 @@ export const testConfig = defineBuilderConfig({
407
420
  { token: TOKENS.Logger, provider: MockLogger }, // ✅ OK - no collision
408
421
  { token: UserService },
409
422
  ],
410
- listeners: []
411
423
  })
412
424
  ```
413
425
 
426
+ ### Listener Uniqueness
427
+
428
+ Similar to tokens, **each (event, listener) pair must be unique** across all partials and the main configuration:
429
+
430
+ ```typescript
431
+ // ❌ ERROR: Duplicate listener in the same configuration
432
+ const config = defineBuilderConfig({
433
+ builderId: 'app',
434
+ injections: [],
435
+ listeners: [
436
+ { event: UserCreatedEvent, listener: EmailNotificationListener },
437
+ { event: UserCreatedEvent, listener: EmailNotificationListener }, // ❌ Duplicate!
438
+ ],
439
+ })
440
+
441
+ // ❌ ERROR: Listener already in partial
442
+ const eventPartial = definePartialConfig({
443
+ listeners: [
444
+ { event: UserCreatedEvent, listener: EmailNotificationListener }
445
+ ],
446
+ })
447
+
448
+ const config = defineBuilderConfig({
449
+ builderId: 'app',
450
+ extends: [eventPartial],
451
+ injections: [],
452
+ listeners: [
453
+ { event: UserCreatedEvent, listener: EmailNotificationListener }, // ❌ Already in partial!
454
+ ],
455
+ })
456
+
457
+ // ✅ OK: Different listener for the same event
458
+ const validConfig = defineBuilderConfig({
459
+ builderId: 'app',
460
+ injections: [],
461
+ listeners: [
462
+ { event: UserCreatedEvent, listener: EmailNotificationListener },
463
+ { event: UserCreatedEvent, listener: SmsNotificationListener }, // ✅ Different listener
464
+ ],
465
+ })
466
+ ```
467
+
468
+ ## Event Programming
469
+
470
+ > **Note**: The `listeners` property is **optional**. If your application doesn't use events, you can omit it entirely from your configuration.
471
+
472
+ ### Why Centralized Event Listeners?
473
+ In traditional event-driven architectures, event listeners are often scattered across the codebase (e.g., manual `dispatcher.on(...)` calls inside constructors or initialization scripts). This makes it hard to visualize the system's reactive flow.
474
+
475
+ WireDI treats event listeners as **part of your application's structural configuration**. By declaring them alongside your dependency injections, you achieve:
476
+ - **🔍 Visibility**: See exactly **who listens to what** in a single configuration file.
477
+ - **🧩 Decoupling**: Services don't need to know about the dispatcher; they just implement `onEvent`.
478
+ - **🛡️ Safety**: Compile-time validation ensures your listener is compatible with the event.
479
+
480
+ ### Usage
481
+ WireDI allows you to bind events to listeners declaratively:
482
+
483
+ ### 1. Enable Event Support
484
+ First, configure the `EventDispatcherProvider` at startup (after the container provider):
485
+
486
+ ```typescript
487
+ import {
488
+ useEventDispatcherProvider,
489
+ MutableEventDispatcherProvider,
490
+ getContainerProvider
491
+ } from '@djodjonx/wiredi'
492
+
493
+ useEventDispatcherProvider(new MutableEventDispatcherProvider({
494
+ containerProvider: getContainerProvider(),
495
+ }))
496
+ ```
497
+
498
+ ### 2. Define Events and Listeners
499
+ Events are simple classes. Listeners are services that implement an `onEvent` method.
500
+
501
+ ```typescript
502
+ // events/UserCreatedEvent.ts
503
+ export class UserCreatedEvent {
504
+ constructor(public readonly user: User) {}
505
+ }
506
+
507
+ // listeners/SendWelcomeEmail.ts
508
+ export class SendWelcomeEmail {
509
+ constructor(private mailer: MailerService) {}
510
+
511
+ onEvent(event: UserCreatedEvent) {
512
+ this.mailer.send(event.user.email, 'Welcome!')
513
+ }
514
+ }
515
+ ```
516
+
517
+ ### 3. Wire in Configuration
518
+ Bind them in your builder configuration using the `listeners` property:
519
+
520
+ ```typescript
521
+ const appConfig = defineBuilderConfig({
522
+ builderId: 'app',
523
+ injections: [
524
+ { token: SendWelcomeEmail }, // Register the listener itself
525
+ { token: MailerService },
526
+ ],
527
+ listeners: [
528
+ // Bind event -> listener
529
+ { event: UserCreatedEvent, listener: SendWelcomeEmail },
530
+ ],
531
+ })
532
+ ```
533
+
534
+ Now, when you dispatch an event:
535
+ ```typescript
536
+ import { getEventDispatcherProvider } from '@djodjonx/wiredi'
537
+
538
+ getEventDispatcherProvider().dispatch(new UserCreatedEvent(newUser))
539
+ // -> SendWelcomeEmail.onEvent() is automatically called
540
+ ```
541
+
414
542
  ## Creating a Custom Provider
415
543
 
416
544
  To use an unsupported DI container, implement the `ContainerProvider` interface:
@@ -496,7 +624,6 @@ open docs/api/index.html
496
624
  - [Quick Start Guide](./README.md#quick-start) - Get started in 4 steps
497
625
  - [Plugin Installation](./README.md#ide-plugin-for-real-time-validation) - IDE integration
498
626
  - [Provider Examples](./examples/) - Integration with tsyringe, Awilix, InversifyJS
499
- - [JSDoc Summary](docs/Agent/JSDOC_SUMMARY.md) - Documentation standards
500
627
 
501
628
  ## Troubleshooting
502
629
 
package/dist/index.cjs CHANGED
@@ -620,7 +620,8 @@ var MutableEventDispatcherProvider = class {
620
620
  for (const listenerToken of listenerTokens) try {
621
621
  this.containerProvider.resolve(listenerToken).onEvent(event);
622
622
  } catch (error) {
623
- console.error(`[MutableEventDispatcherProvider] Error dispatching event "${eventName}":`, error.stack || error.message);
623
+ const errorMessage = error instanceof Error ? error.stack || error.message : String(error);
624
+ console.error(`[MutableEventDispatcherProvider] Error dispatching event "${eventName}":`, errorMessage);
624
625
  throw error;
625
626
  }
626
627
  }
@@ -631,6 +632,13 @@ var MutableEventDispatcherProvider = class {
631
632
  return listeners !== void 0 && listeners.length > 0;
632
633
  }
633
634
  /** @inheritdoc */
635
+ hasListener(eventToken, listenerToken) {
636
+ const eventName = this.getEventName(eventToken);
637
+ const listeners = this.listeners.get(eventName);
638
+ if (!listeners) return false;
639
+ return listeners.includes(listenerToken);
640
+ }
641
+ /** @inheritdoc */
634
642
  clearListeners(eventToken) {
635
643
  const eventName = this.getEventName(eventToken);
636
644
  this.listeners.delete(eventName);
@@ -765,11 +773,11 @@ function registerConfig(provider, injections, context) {
765
773
  });
766
774
  }
767
775
  function registerEvent(_provider, listeners) {
768
- if (!listeners.length) return;
776
+ if (!listeners || !listeners.length) return;
769
777
  if (!hasEventDispatcherProvider()) return;
770
778
  const eventDispatcher = getEventDispatcherProvider();
771
779
  listeners.forEach((configEntry) => {
772
- eventDispatcher.register(configEntry.event, configEntry.listener);
780
+ if (!eventDispatcher.hasListener(configEntry.event, configEntry.listener)) eventDispatcher.register(configEntry.event, configEntry.listener);
773
781
  });
774
782
  }
775
783
  /**
@@ -864,7 +872,7 @@ function definePartialConfig(config) {
864
872
  */
865
873
  function defineBuilderConfig(config) {
866
874
  const mergedInjections = [...config.injections, ...(config.extends || []).flatMap((p) => p.injections || [])];
867
- const mergedListeners = [...(config.extends || []).flatMap((p) => p.listeners || []), ...config.listeners];
875
+ const mergedListeners = [...(config.extends || []).flatMap((p) => p.listeners || []), ...config.listeners || []];
868
876
  return {
869
877
  ...config,
870
878
  injections: mergedInjections,