@djodjonx/neo-syringe 1.2.0 → 1.2.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.
Files changed (42) hide show
  1. package/.github/workflows/docs.yml +59 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +72 -779
  4. package/dist/cli/index.cjs +15 -0
  5. package/dist/cli/index.mjs +15 -0
  6. package/dist/index.d.cts +1 -1
  7. package/dist/index.d.mts +1 -1
  8. package/dist/unplugin/index.cjs +31 -7
  9. package/dist/unplugin/index.d.cts +7 -5
  10. package/dist/unplugin/index.d.mts +7 -5
  11. package/dist/unplugin/index.mjs +31 -7
  12. package/docs/.vitepress/config.ts +109 -0
  13. package/docs/.vitepress/theme/custom.css +150 -0
  14. package/docs/.vitepress/theme/index.ts +17 -0
  15. package/docs/api/configuration.md +274 -0
  16. package/docs/api/functions.md +291 -0
  17. package/docs/api/types.md +158 -0
  18. package/docs/guide/basic-usage.md +267 -0
  19. package/docs/guide/cli.md +174 -0
  20. package/docs/guide/generated-code.md +284 -0
  21. package/docs/guide/getting-started.md +171 -0
  22. package/docs/guide/ide-plugin.md +203 -0
  23. package/docs/guide/injection-types.md +287 -0
  24. package/docs/guide/legacy-migration.md +333 -0
  25. package/docs/guide/lifecycle.md +223 -0
  26. package/docs/guide/parent-container.md +321 -0
  27. package/docs/guide/scoped-injections.md +271 -0
  28. package/docs/guide/what-is-neo-syringe.md +162 -0
  29. package/docs/guide/why-neo-syringe.md +219 -0
  30. package/docs/index.md +138 -0
  31. package/docs/public/logo.png +0 -0
  32. package/package.json +5 -3
  33. package/src/analyzer/types.ts +52 -52
  34. package/src/cli/index.ts +15 -0
  35. package/src/generator/Generator.ts +23 -1
  36. package/src/types.ts +1 -1
  37. package/src/unplugin/index.ts +13 -41
  38. package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
  39. package/tests/e2e/container-integration.test.ts +19 -19
  40. package/tests/e2e/generated-code.test.ts +2 -2
  41. package/tsconfig.json +2 -1
  42. package/typedoc.json +0 -5
package/README.md CHANGED
@@ -5,821 +5,142 @@
5
5
  <p align="center">
6
6
  <a href="https://www.npmjs.com/package/@djodjonx/neo-syringe"><img src="https://img.shields.io/npm/v/@djodjonx/neo-syringe.svg?style=flat-square" alt="npm version"></a>
7
7
  <a href="https://github.com/djodjonx/neo-syringe/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/djodjonx/neo-syringe/ci.yml?style=flat-square&label=tests" alt="Tests"></a>
8
- <a href="https://github.com/djodjonx/neo-syringe/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/djodjonx/neo-syringe/ci.yml?style=flat-square" alt="CI"></a>
9
8
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-blue.svg?style=flat-square" alt="TypeScript"></a>
10
9
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square" alt="License: MIT"></a>
10
+ <a href="https://djodjonx.github.io/neo-syringe/"><img src="https://img.shields.io/badge/docs-VitePress-0d9488.svg?style=flat-square" alt="Documentation"></a>
11
11
  </p>
12
12
 
13
13
  <h1 align="center">Zero-Overhead, Compile-Time Dependency Injection</h1>
14
14
 
15
15
  <p align="center">
16
- <strong>Neo-Syringe</strong> is a next-generation DI system that shifts resolution from <strong>Runtime</strong> to <strong>Build-Time</strong>. It eliminates reflection and metadata in favor of generated, optimized JavaScript factories.
16
+ <strong>Neo-Syringe</strong> shifts DI resolution from <strong>Runtime</strong> to <strong>Build-Time</strong>.<br>
17
+ No reflection, no decorators, just pure TypeScript.
17
18
  </p>
18
19
 
19
- ---
20
-
21
- `neo-syringe` allows you to:
22
-
23
- - ✨ **Use Interfaces as Tokens**: Native support for `useInterface<ILogger>()` without manual Symbols
24
- - 🚀 **Zero Runtime Overhead**: No reflection, no `reflect-metadata`, just pure factory functions
25
- - 🛡️ **Compile-Time Safety**: Detect circular dependencies, missing bindings, and type mismatches instantly
26
- - 🔄 **Migrate Gradually**: Use `useContainer` to bridge existing containers (like `tsyringe`)
27
- - 🤖 **Validate in CI**: Standalone CLI to verify your dependency graph before deployment
28
-
29
- ## 📚 Documentation
30
-
31
- - **[Quick Start](#quick-start)** - Wire your first application in 5 minutes
32
- - **[Injection Types](#injection-types)** - All ways to define dependencies
33
- - **[Generated Code](#generated-code-example)** - See what the compiler produces
34
- - **[Parent Container](#parent-container-usecontainer)** - SharedKernel and modular architecture
35
- - **[Legacy Migration](#legacy-migration-usecontainer-1)** - Bridge existing containers (tsyringe, etc.)
36
- - **[CLI Validator](#cli-validator)** - Ensure graph integrity in CI/CD
37
- - **[IDE Support](#ide-plugin-for-real-time-validation)** - Get real-time errors in VS Code
38
-
39
- ## Why Neo-Syringe?
40
-
41
- Traditional containers (InversifyJS, tsyringe) or modern wrappers (WireDI) rely on **Runtime Resolution**. This means:
42
-
43
- - ❌ You ship the DI container logic to the browser
44
- - ❌ Errors (missing bindings) happen at runtime
45
- - ❌ Interfaces are erased, requiring manual Symbols
46
-
47
- **Neo-Syringe is different.** It works as a **Compiler Plugin**:
48
-
49
- ### 1. Code Generation
50
- Neo-Syringe analyzes your configuration and generates a specialized TypeScript class with hardcoded `new Service(new Dep())` calls. **Zero runtime resolution overhead**.
20
+ <p align="center">
21
+ <a href="https://djodjonx.github.io/neo-syringe/"><strong>📚 Read the Documentation →</strong></a>
22
+ </p>
51
23
 
52
- ### 2. Interface Support
53
- It automatically generates unique IDs for interfaces at build time. Write `useInterface<ILogger>()` instead of managing Symbols manually.
24
+ <p align="center">
25
+ <a href="https://djodjonx.github.io/neo-syringe/guide/getting-started">Getting Started</a>
26
+ <a href="https://djodjonx.github.io/neo-syringe/guide/why-neo-syringe">Why Neo-Syringe?</a> •
27
+ <a href="https://djodjonx.github.io/neo-syringe/api/types">API Reference</a>
28
+ </p>
54
29
 
55
- ### 3. Pure Classes
56
- Your business classes stay **100% pure** - no decorators, no DI imports, no framework coupling.
30
+ ---
57
31
 
58
- ### 4. Real-Time Validation
59
- Errors are detected **in your IDE** instantly:
32
+ ## Features
60
33
 
61
- ```typescript
62
- // Error detected in IDE: "ILogger" has no registered provider
63
- const config = defineBuilderConfig({
64
- injections: [
65
- { token: UserService }, // UserService depends on ILogger
66
- ],
67
- })
68
- ```
34
+ - **Use Interfaces as Tokens** - `useInterface<ILogger>()` without manual Symbols
35
+ - **Zero Runtime Overhead** - Generated factory functions, no DI library shipped
36
+ - **Compile-Time Safety** - Errors detected in your IDE, not at runtime
37
+ - **Pure Classes** - No decorators, no DI imports in your business code
38
+ - **Gradual Migration** - Bridge existing containers (tsyringe, InversifyJS)
39
+ - **CI Validation** - CLI to verify your dependency graph
69
40
 
70
- ## Installation
41
+ ## 📦 Installation
71
42
 
72
43
  ```bash
73
- # With npm
44
+ # npm
74
45
  npm install @djodjonx/neo-syringe
75
46
  npm install -D unplugin
76
47
 
77
- # With pnpm
48
+ # pnpm
78
49
  pnpm add @djodjonx/neo-syringe
79
50
  pnpm add -D unplugin
80
51
  ```
81
52
 
82
- > **Note**: `typescript` (>=5.0.0) and `unplugin` are required peer dependencies.
83
-
84
- ### Build System Integration
85
-
86
- Configure the plugin in your bundler to enable compile-time code generation.
87
-
88
- <details>
89
- <summary><strong>Vite</strong></summary>
90
-
91
- ```typescript
92
- // vite.config.ts
93
- import { defineConfig } from 'vite';
94
- import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
95
-
96
- export default defineConfig({
97
- plugins: [neoSyringePlugin.vite()]
98
- });
99
- ```
100
-
101
- </details>
102
-
103
- <details>
104
- <summary><strong>Rollup</strong></summary>
105
-
106
- ```typescript
107
- // rollup.config.js
108
- import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
109
-
110
- export default {
111
- plugins: [neoSyringePlugin.rollup()]
112
- };
113
- ```
114
-
115
- </details>
116
-
117
- <details>
118
- <summary><strong>Webpack</strong></summary>
119
-
120
- ```typescript
121
- // webpack.config.js
122
- module.exports = {
123
- plugins: [require('@djodjonx/neo-syringe/plugin').webpack()]
124
- };
125
- ```
126
-
127
- </details>
128
-
129
- ### Dev Mode & HMR Support
130
-
131
- ✅ **The plugin works in dev mode** (Vite, Rollup watch, Webpack dev server).
132
-
133
- The `transform` hook is called on every file change, so your container is regenerated instantly during development with full Hot Module Replacement (HMR) support.
134
-
135
- > ⚠️ **Best Practice**: Put your container configuration in a **dedicated file** (e.g., `container.ts`). The plugin replaces the entire file content with generated code, so mixing container config with other exports may cause issues.
136
-
137
- ```
138
- src/
139
- ├── container.ts # ✅ Dedicated file for defineBuilderConfig
140
- ├── services/
141
- │ ├── logger.ts
142
- │ └── user.service.ts
143
- └── main.ts
144
- ```
145
-
146
- ## Quick Start
147
-
148
- ### 1. Define Services (Pure TypeScript)
149
-
150
- No decorators required. Just plain classes and interfaces.
53
+ ## 🚀 Quick Example
151
54
 
152
55
  ```typescript
153
- // logger.ts
154
- export interface ILogger {
56
+ // Pure TypeScript - no decorators!
57
+ interface ILogger {
155
58
  log(msg: string): void;
156
59
  }
157
60
 
158
- export class ConsoleLogger implements ILogger {
61
+ class ConsoleLogger implements ILogger {
159
62
  log(msg: string) { console.log(msg); }
160
63
  }
161
64
 
162
- // user.service.ts
163
- export class UserService {
164
- constructor(private logger: ILogger) {} // Dependency is an Interface!
65
+ class UserService {
66
+ constructor(private logger: ILogger) {}
165
67
  }
166
68
  ```
167
69
 
168
- ### 2. Configure the Container
169
-
170
- Use `defineBuilderConfig` to wire your graph.
171
-
172
70
  ```typescript
173
- // config.ts
71
+ // container.ts
174
72
  import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
175
- import { ILogger, ConsoleLogger } from './logger';
176
- import { UserService } from './user.service';
177
-
178
- export const appConfig = defineBuilderConfig({
179
- name: 'AppModule',
180
- injections: [
181
- // Bind Interface -> Implementation
182
- { token: useInterface<ILogger>(), provider: ConsoleLogger },
183
-
184
- // Autowire Class
185
- { token: UserService }
186
- ]
187
- });
188
- ```
189
-
190
- ### 3. Use the Container
191
-
192
- The compiler plugin replaces the configuration with a generated container class.
193
-
194
- ```typescript
195
- // main.ts
196
- import { appConfig } from './config';
197
-
198
- const container = appConfig; // This IS the container at runtime!
199
- const userService = container.resolve(UserService);
200
- ```
201
-
202
- ---
203
-
204
- ## Injection Types
205
-
206
- ### Class Token (Autowire)
207
-
208
- ```typescript
209
- { token: UserService }
210
- ```
211
-
212
- ### Interface Token
213
-
214
- ```typescript
215
- { token: useInterface<ILogger>(), provider: ConsoleLogger }
216
- ```
217
-
218
- ### Explicit Provider
219
-
220
- ```typescript
221
- { token: UserService, provider: MockUserService }
222
- ```
223
-
224
- ### Factory Provider
225
-
226
- Use factory functions for dynamic instantiation or container access.
227
-
228
- ```typescript
229
- // Auto-detected: arrow functions are treated as factories
230
- {
231
- token: useInterface<IConfig>(),
232
- provider: (container) => ({
233
- apiUrl: process.env.API_URL ?? 'http://localhost',
234
- timeout: 5000
235
- })
236
- }
237
-
238
- // Explicit factory flag
239
- {
240
- token: useInterface<IDatabase>(),
241
- provider: createDatabaseConnection,
242
- useFactory: true
243
- }
244
-
245
- // Factory with dependencies
246
- {
247
- token: useInterface<IService>(),
248
- provider: (container) => {
249
- const logger = container.resolve(useInterface<ILogger>());
250
- return new MyService(logger);
251
- }
252
- }
253
- ```
254
-
255
- ### Primitive Values with `useProperty`
256
-
257
- Inject primitives (string, number, boolean) while keeping classes **pure**.
258
-
259
- ```typescript
260
- import { defineBuilderConfig, useProperty } from '@djodjonx/neo-syringe';
261
-
262
- // Pure class - no DI imports!
263
- class ApiService {
264
- constructor(
265
- private apiUrl: string,
266
- private maxRetries: number
267
- ) {}
268
- }
269
-
270
- // Define property tokens
271
- const apiUrl = useProperty<string>(ApiService, 'apiUrl');
272
- const maxRetries = useProperty<number>(ApiService, 'maxRetries');
273
-
274
- export const config = defineBuilderConfig({
275
- injections: [
276
- { token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
277
- { token: maxRetries, provider: () => 5 },
278
- { token: ApiService } // Auto-wires primitives!
279
- ]
280
- });
281
- ```
282
-
283
- **Benefits:**
284
- - ✅ No collision: `useProperty(ApiService, 'url')` ≠ `useProperty(AuthService, 'url')`
285
- - ✅ LSP validation: Detects if parameter doesn't exist
286
- - ✅ Refactoring-friendly: Rename parameter → LSP error
287
-
288
- ### Lifecycle
289
-
290
- ```typescript
291
- { token: UserService, lifecycle: 'transient' } // Default is 'singleton'
292
-
293
- // Factories support lifecycle too
294
- {
295
- token: useInterface<IRequest>(),
296
- provider: () => ({ id: crypto.randomUUID() }),
297
- lifecycle: 'transient'
298
- }
299
- ```
300
-
301
- ### Scoped Injections (`scoped: true`)
302
-
303
- Override a token from a parent container **without causing a duplicate error**. Useful for testing or module-specific overrides.
304
-
305
- ```typescript
306
- // SharedKernel - Production logger
307
- const sharedKernel = defineBuilderConfig({
308
- name: 'SharedKernel',
309
- injections: [
310
- { token: useInterface<ILogger>(), provider: ConsoleLogger, lifecycle: 'singleton' }
311
- ]
312
- });
313
-
314
- // TestModule - Override with MockLogger, scoped to this container
315
- const testModule = defineBuilderConfig({
316
- name: 'TestModule',
317
- useContainer: sharedKernel,
318
- injections: [
319
- {
320
- token: useInterface<ILogger>(),
321
- provider: MockLogger,
322
- lifecycle: 'transient', // Can use different lifecycle than parent
323
- scoped: true // 👈 Allows override without duplicate error
324
- },
325
- { token: UserService } // Uses MockLogger from this container
326
- ]
327
- });
328
- ```
329
-
330
- **Behavior:**
331
-
332
- | Scenario | Without `scoped` | With `scoped: true` |
333
- |----------|------------------|---------------------|
334
- | Token exists in parent | ❌ Duplicate error | ✅ Override locally |
335
- | Token does not exist in parent | ✅ OK | ✅ OK |
336
- | Resolution | Delegates to parent | Uses local instance |
337
-
338
- **Use Cases:**
339
- - 🧪 **Testing**: Override production services with mocks
340
- - 🔧 **Module isolation**: Each module has its own instance
341
- - ⚙️ **Different lifecycle**: Parent uses singleton, child uses transient
342
-
343
- ---
344
-
345
- ## Generated Code Example
346
-
347
- To understand how Neo-Syringe works, here's what your code looks like **before** and **after** compilation.
348
-
349
- ### Before (Your Configuration)
350
-
351
- ```typescript
352
- // config.ts
353
- import { defineBuilderConfig, useInterface, useProperty } from '@djodjonx/neo-syringe';
354
-
355
- interface ILogger { log(msg: string): void; }
356
- class ConsoleLogger implements ILogger {
357
- log(msg: string) { console.log(msg); }
358
- }
359
-
360
- class ApiService {
361
- constructor(
362
- private logger: ILogger,
363
- private apiUrl: string
364
- ) {}
365
- }
366
-
367
- const apiUrl = useProperty<string>(ApiService, 'apiUrl');
368
73
 
369
74
  export const container = defineBuilderConfig({
370
- name: 'AppContainer',
371
- injections: [
372
- { token: useInterface<ILogger>(), provider: ConsoleLogger },
373
- { token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
374
- { token: ApiService }
375
- ]
376
- });
377
- ```
378
-
379
- ### After (Generated Code)
380
-
381
- The build plugin replaces `defineBuilderConfig(...)` with an optimized container class:
382
-
383
- ```typescript
384
- // config.ts (after build)
385
- import * as Import_0 from './services';
386
-
387
- // -- Factories (generated) --
388
- function create_ILogger(container: NeoContainer) {
389
- return new Import_0.ConsoleLogger();
390
- }
391
-
392
- function create_ApiService_apiUrl(container: NeoContainer) {
393
- const userFactory = () => process.env.API_URL ?? 'http://localhost';
394
- return userFactory(container);
395
- }
396
-
397
- function create_ApiService(container: NeoContainer) {
398
- return new Import_0.ApiService(
399
- container.resolve("ILogger"),
400
- container.resolve("PropertyToken:ApiService.apiUrl")
401
- );
402
- }
403
-
404
- // -- Container (generated) --
405
- export class NeoContainer {
406
- private instances = new Map<any, any>();
407
-
408
- constructor(
409
- private parent?: any,
410
- private legacy?: any[],
411
- private name: string = 'AppContainer'
412
- ) {}
413
-
414
- public resolve(token: any): any {
415
- const result = this.resolveLocal(token);
416
- if (result !== undefined) return result;
417
-
418
- if (this.parent) {
419
- try { return this.parent.resolve(token); }
420
- catch (e) { /* fallback */ }
421
- }
422
-
423
- throw new Error(`[${this.name}] Service not found: ${token}`);
424
- }
425
-
426
- private resolveLocal(token: any): any {
427
- // Interface token (string)
428
- if (token === "ILogger") {
429
- if (!this.instances.has("ILogger")) {
430
- this.instances.set("ILogger", create_ILogger(this));
431
- }
432
- return this.instances.get("ILogger");
433
- }
434
-
435
- // PropertyToken (string)
436
- if (token === "PropertyToken:ApiService.apiUrl") {
437
- if (!this.instances.has("PropertyToken:ApiService.apiUrl")) {
438
- this.instances.set("PropertyToken:ApiService.apiUrl", create_ApiService_apiUrl(this));
439
- }
440
- return this.instances.get("PropertyToken:ApiService.apiUrl");
441
- }
442
-
443
- // Class token (reference)
444
- if (token === Import_0.ApiService) {
445
- if (!this.instances.has(Import_0.ApiService)) {
446
- this.instances.set(Import_0.ApiService, create_ApiService(this));
447
- }
448
- return this.instances.get(Import_0.ApiService);
449
- }
450
-
451
- return undefined;
452
- }
453
- }
454
-
455
- export const container = new NeoContainer();
456
- ```
457
-
458
- ### Key Observations
459
-
460
- | Aspect | Before | After |
461
- |--------|--------|-------|
462
- | **Resolution** | Runtime lookup | Direct `new` calls |
463
- | **Interfaces** | Type-erased | String IDs generated |
464
- | **Dependencies** | Analyzed at runtime | Hardcoded in factories |
465
- | **Container size** | Full DI library | ~50 lines of code |
466
- | **Performance** | Map lookups + reflection | Direct instantiation |
467
-
468
- > 💡 **The generated code has zero dependencies** - no DI library shipped to production!
469
-
470
- ---
471
-
472
- ## Advanced Features
473
-
474
- ### Parent Container (`useContainer`)
475
-
476
- Neo-Syringe supports **hierarchical containers**. A child container can delegate resolution to a parent container for tokens it doesn't define locally.
477
-
478
- #### Example: SharedKernel Architecture
479
-
480
- Perfect for modular applications where core services are shared across bounded contexts.
481
-
482
- ```typescript
483
- // shared-kernel/container.ts
484
- import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
485
-
486
- // Shared interfaces
487
- export interface ILogger { log(msg: string): void; }
488
- export interface IEventBus { publish(event: any): void; }
489
-
490
- class ConsoleLogger implements ILogger {
491
- log(msg: string) { console.log(`[LOG] ${msg}`); }
492
- }
493
-
494
- class InMemoryEventBus implements IEventBus {
495
- publish(event: any) { console.log('Event:', event); }
496
- }
497
-
498
- // SharedKernel container - reusable across modules
499
- export const sharedKernel = defineBuilderConfig({
500
- name: 'SharedKernel',
501
75
  injections: [
502
76
  { token: useInterface<ILogger>(), provider: ConsoleLogger },
503
- { token: useInterface<IEventBus>(), provider: InMemoryEventBus }
504
- ]
505
- });
506
- ```
507
-
508
- ```typescript
509
- // user-module/container.ts
510
- import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
511
- import { sharedKernel, ILogger, IEventBus } from '../shared-kernel/container';
512
-
513
- // User module services
514
- class UserRepository {
515
- findById(id: string) { return { id, name: 'John' }; }
516
- }
517
-
518
- class UserService {
519
- constructor(
520
- private logger: ILogger, // From SharedKernel!
521
- private eventBus: IEventBus, // From SharedKernel!
522
- private repo: UserRepository // Local to this module
523
- ) {}
524
- }
525
-
526
- export const userModule = defineBuilderConfig({
527
- name: 'UserModule',
528
- useContainer: sharedKernel, // 👈 Inherit from SharedKernel
529
- injections: [
530
- { token: UserRepository },
531
77
  { token: UserService }
532
78
  ]
533
79
  });
534
- ```
535
-
536
- ```typescript
537
- // order-module/container.ts
538
- import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
539
- import { sharedKernel, ILogger } from '../shared-kernel/container';
540
-
541
- class OrderService {
542
- constructor(private logger: ILogger) {} // From SharedKernel!
543
- }
544
-
545
- export const orderModule = defineBuilderConfig({
546
- name: 'OrderModule',
547
- useContainer: sharedKernel, // 👈 Same SharedKernel, different module
548
- injections: [
549
- { token: OrderService }
550
- ]
551
- });
552
- ```
553
-
554
- ```typescript
555
- // main.ts
556
- import { userModule } from './user-module/container';
557
- import { orderModule } from './order-module/container';
558
-
559
- // Both modules share the same ILogger and IEventBus instances!
560
- const userService = userModule.resolve(UserService);
561
- const orderService = orderModule.resolve(OrderService);
562
- ```
563
-
564
- #### Multi-Level Hierarchy
565
-
566
- You can chain containers for complex architectures:
567
80
 
568
- ```typescript
569
- // Level 1: Infrastructure
570
- const infrastructure = defineBuilderConfig({
571
- name: 'Infrastructure',
572
- injections: [
573
- { token: useInterface<ILogger>(), provider: ConsoleLogger },
574
- { token: useInterface<IDatabase>(), provider: PostgresDatabase }
575
- ]
576
- });
577
-
578
- // Level 2: Domain (inherits Infrastructure)
579
- const domain = defineBuilderConfig({
580
- name: 'Domain',
581
- useContainer: infrastructure,
582
- injections: [
583
- { token: UserRepository },
584
- { token: OrderRepository }
585
- ]
586
- });
587
-
588
- // Level 3: Application (inherits Domain + Infrastructure)
589
- const application = defineBuilderConfig({
590
- name: 'Application',
591
- useContainer: domain, // Gets Domain AND Infrastructure!
592
- injections: [
593
- { token: UserService },
594
- { token: OrderService }
595
- ]
596
- });
597
- ```
598
-
599
- ---
600
-
601
- ### Legacy Migration (`useContainer`)
602
-
603
- The same `useContainer` mechanism works with legacy DI containers (tsyringe, InversifyJS, Awilix). Neo-Syringe can delegate resolution to any container that has a `resolve()` method.
604
-
605
- #### Example with tsyringe
606
-
607
- ```typescript
608
- // legacy-container.ts (existing tsyringe setup)
609
- import 'reflect-metadata';
610
- import { container, injectable } from 'tsyringe';
611
-
612
- @injectable()
613
- export class AuthService {
614
- validateToken(token: string) { return true; }
615
- }
616
-
617
- @injectable()
618
- export class LegacyUserRepository {
619
- findById(id: string) { return { id, name: 'John' }; }
620
- }
621
-
622
- // Register in tsyringe
623
- container.registerSingleton(AuthService);
624
- container.registerSingleton(LegacyUserRepository);
625
-
626
- export { container as legacyContainer };
627
- ```
628
-
629
- ```typescript
630
- // container.ts (neo-syringe bridging to tsyringe)
631
- import { defineBuilderConfig, declareContainerTokens, useInterface } from '@djodjonx/neo-syringe';
632
- import { legacyContainer, AuthService, LegacyUserRepository } from './legacy-container';
633
-
634
- // Declare what the legacy container provides (for type-safety)
635
- const legacy = declareContainerTokens<{
636
- AuthService: AuthService;
637
- LegacyUserRepository: LegacyUserRepository;
638
- }>(legacyContainer);
639
-
640
- // New services using Neo-Syringe
641
- interface ILogger { log(msg: string): void; }
642
- class ConsoleLogger implements ILogger {
643
- log(msg: string) { console.log(msg); }
644
- }
645
-
646
- class UserService {
647
- constructor(
648
- private auth: AuthService, // From legacy container!
649
- private repo: LegacyUserRepository, // From legacy container!
650
- private logger: ILogger // From neo-syringe
651
- ) {}
652
- }
653
-
654
- export const appContainer = defineBuilderConfig({
655
- name: 'AppContainer',
656
- useContainer: legacy, // 👈 Bridge to legacy container
657
- injections: [
658
- { token: useInterface<ILogger>(), provider: ConsoleLogger },
659
- { token: UserService } // Dependencies resolved from both containers!
660
- ]
661
- });
662
- ```
663
-
664
- ```typescript
665
- // main.ts
666
- import { appContainer } from './container';
667
-
668
- const userService = appContainer.resolve(UserService);
669
- // ✅ AuthService and LegacyUserRepository come from tsyringe
670
- // ✅ ILogger comes from neo-syringe
81
+ // Use it
82
+ const userService = container.resolve(UserService);
671
83
  ```
672
84
 
673
- #### Example with InversifyJS
85
+ At build time, this generates optimized factory functions. **Zero DI library shipped to production!**
674
86
 
675
- ```typescript
676
- // legacy-inversify.ts
677
- import 'reflect-metadata';
678
- import { Container, injectable } from 'inversify';
87
+ ## 📖 Documentation
679
88
 
680
- @injectable()
681
- class DatabaseConnection {
682
- query(sql: string) { return []; }
683
- }
89
+ For complete documentation, visit **[djodjonx.github.io/neo-syringe](https://djodjonx.github.io/neo-syringe/)**
684
90
 
685
- const inversifyContainer = new Container();
686
- inversifyContainer.bind(DatabaseConnection).toSelf().inSingletonScope();
91
+ | Topic | Description |
92
+ |-------|-------------|
93
+ | [Getting Started](https://djodjonx.github.io/neo-syringe/guide/getting-started) | Installation and first container |
94
+ | [Why Neo-Syringe?](https://djodjonx.github.io/neo-syringe/guide/why-neo-syringe) | Comparison with traditional DI |
95
+ | [Injection Types](https://djodjonx.github.io/neo-syringe/guide/injection-types) | Classes, interfaces, factories, primitives |
96
+ | [Lifecycle](https://djodjonx.github.io/neo-syringe/guide/lifecycle) | Singleton vs transient |
97
+ | [Scoped Injections](https://djodjonx.github.io/neo-syringe/guide/scoped-injections) | Override parent container tokens |
98
+ | [Parent Container](https://djodjonx.github.io/neo-syringe/guide/parent-container) | SharedKernel architecture |
99
+ | [Legacy Migration](https://djodjonx.github.io/neo-syringe/guide/legacy-migration) | Bridge tsyringe, InversifyJS |
100
+ | [Generated Code](https://djodjonx.github.io/neo-syringe/guide/generated-code) | What the compiler produces |
101
+ | [CLI Validator](https://djodjonx.github.io/neo-syringe/guide/cli) | Validate in CI/CD |
102
+ | [IDE Plugin](https://djodjonx.github.io/neo-syringe/guide/ide-plugin) | Real-time error detection |
103
+ | [API Reference](https://djodjonx.github.io/neo-syringe/api/types) | Types and functions |
104
+
105
+ ## 🔧 Build Plugin Setup
687
106
 
688
- export { inversifyContainer, DatabaseConnection };
689
- ```
107
+ <details>
108
+ <summary><strong>Vite</strong></summary>
690
109
 
691
110
  ```typescript
692
- // container.ts
693
- import { defineBuilderConfig, declareContainerTokens } from '@djodjonx/neo-syringe';
694
- import { inversifyContainer, DatabaseConnection } from './legacy-inversify';
695
-
696
- const legacy = declareContainerTokens<{
697
- DatabaseConnection: DatabaseConnection;
698
- }>(inversifyContainer);
699
-
700
- class ReportService {
701
- constructor(private db: DatabaseConnection) {} // From Inversify!
702
- }
111
+ import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
703
112
 
704
- export const container = defineBuilderConfig({
705
- useContainer: legacy,
706
- injections: [
707
- { token: ReportService }
708
- ]
113
+ export default defineConfig({
114
+ plugins: [neoSyringePlugin.vite()]
709
115
  });
710
116
  ```
117
+ </details>
711
118
 
712
- #### How Resolution Works
713
-
714
- When `container.resolve(Token)` is called:
715
-
716
- ```
717
- 1. Neo-Syringe checks its own registrations
718
- └── Found? → Return instance
719
- └── Not found? ↓
720
-
721
- 2. Delegate to parent container (useContainer)
722
- └── Found? → Return instance
723
- └── Not found? ↓
724
-
725
- 3. Throw "Service not found" error
726
- ```
727
-
728
- > 💡 **Migration Strategy**: Start by bridging your entire legacy container, then gradually move services to Neo-Syringe. Once all services are migrated, remove `useContainer`.
729
-
730
- #### How Legacy Bridging Works Internally
731
-
732
- **At Compile-Time:**
733
-
734
- 1. `declareContainerTokens<T>()` is analyzed
735
- 2. The type `T` properties are extracted (e.g., `{ AuthService, UserRepo }`)
736
- 3. These tokens are added to `parentProvidedTokens`
737
- 4. The GraphValidator accepts these as valid dependencies
738
- 5. The Generator outputs: `new NeoContainer(undefined, [legacyContainer], 'MyApp')`
739
-
740
- **At Runtime:**
741
-
742
- ```typescript
743
- // Generated code (simplified)
744
- class NeoContainer {
745
- constructor(
746
- private parent?: any,
747
- private legacy?: any[], // ← Your tsyringe/inversify container
748
- private name?: string
749
- ) {}
750
-
751
- resolve(token: any): any {
752
- // 1. Try local resolution
753
- const local = this.resolveLocal(token);
754
- if (local !== undefined) return local;
755
-
756
- // 2. Delegate to legacy containers
757
- if (this.legacy) {
758
- for (const container of this.legacy) {
759
- try {
760
- return container.resolve(token); // ← Calls tsyringe.resolve()!
761
- } catch (e) { /* try next */ }
762
- }
763
- }
764
-
765
- throw new Error(`Service not found: ${token}`);
766
- }
767
- }
768
- ```
769
-
770
- **Validation Features:**
771
-
772
- | Check | Description |
773
- |-------|-------------|
774
- | ✅ Missing binding | Error if dependency not in local OR legacy container |
775
- | ✅ Duplicate detection | Error if token already registered in parent/legacy |
776
- | ✅ Type safety | `declareContainerTokens<T>()` provides TypeScript types |
777
-
778
- ### Partials (Modular Config)
779
-
780
- Split configuration into reusable blocks.
119
+ <details>
120
+ <summary><strong>Rollup</strong></summary>
781
121
 
782
122
  ```typescript
783
- // logging.partial.ts
784
- export const loggingConfig = definePartialConfig({
785
- injections: [
786
- { token: useInterface<ILogger>(), provider: ConsoleLogger }
787
- ]
788
- });
123
+ import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
789
124
 
790
- // app.config.ts
791
- export const appConfig = defineBuilderConfig({
792
- extends: [loggingConfig], // Inherit injections
793
- injections: [
794
- { token: UserService }
795
- ]
796
- });
125
+ export default {
126
+ plugins: [neoSyringePlugin.rollup()]
127
+ };
797
128
  ```
129
+ </details>
798
130
 
799
- ### CLI Validator
800
-
801
- Validate your dependency graph in CI/CD pipelines.
802
-
803
- ```bash
804
- npx neo-syringe
805
- ```
131
+ <details>
132
+ <summary><strong>Webpack</strong></summary>
806
133
 
807
- **Output:**
808
- ```
809
- Analyzing project: /path/to/tsconfig.json
810
- 🔍 Extracting dependency graph...
811
- Found 45 services.
812
- 🛡️ Validating graph...
813
- ✅ Validation passed! No circular dependencies or missing bindings found.
134
+ ```javascript
135
+ module.exports = {
136
+ plugins: [require('@djodjonx/neo-syringe/plugin').webpack()]
137
+ };
814
138
  ```
139
+ </details>
815
140
 
816
- ---
817
-
818
- ## IDE Plugin for Real-Time Validation
819
-
820
- Get immediate feedback on configuration errors.
141
+ ## 🛡️ IDE Support
821
142
 
822
- ### 1. Add the plugin to `tsconfig.json`
143
+ Add to `tsconfig.json` for real-time error detection:
823
144
 
824
145
  ```json
825
146
  {
@@ -831,35 +152,7 @@ Get immediate feedback on configuration errors.
831
152
  }
832
153
  ```
833
154
 
834
- ### 2. Use Workspace TypeScript
835
-
836
- In VS Code: `Ctrl+Shift+P` → "TypeScript: Select TypeScript Version" → "Use Workspace Version"
837
-
838
- ### Detected Errors
839
-
840
- | Error | Message |
841
- | :--- | :--- |
842
- | 🔴 **Circular Dependency** | `[Neo-Syringe] Circular dependency detected: A -> B -> A` |
843
- | 🔴 **Missing Binding** | `[Neo-Syringe] Missing binding: 'Service' depends on 'I', but no provider registered.` |
844
- | 🔴 **Duplicate** | `[Neo-Syringe] Duplicate registration: 'Service' is already registered.` |
845
-
846
- ---
847
-
848
- ## Troubleshooting
849
-
850
- ### The plugin doesn't detect errors
851
-
852
- 1. Verify TypeScript uses the workspace version
853
- 2. Restart the TypeScript server (`Cmd+Shift+P` → "TypeScript: Restart TS Server")
854
- 3. Check that `@djodjonx/neo-syringe/lsp` is in your `tsconfig.json` plugins
855
-
856
- ### Build plugin not working
857
-
858
- Ensure `unplugin` is installed and the plugin is correctly configured in your bundler.
859
-
860
- ---
861
-
862
- ## License
155
+ ## 📄 License
863
156
 
864
157
  MIT
865
158