@djodjonx/neo-syringe 1.1.5 → 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 (61) hide show
  1. package/.github/workflows/ci.yml +6 -5
  2. package/.github/workflows/docs.yml +59 -0
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +74 -740
  5. package/dist/{GraphValidator-G0F4QiLk.cjs → GraphValidator-CV4VoJl0.cjs} +18 -10
  6. package/dist/{GraphValidator-C8ldJtNp.mjs → GraphValidator-DXqqkNdS.mjs} +18 -10
  7. package/dist/cli/index.cjs +16 -1
  8. package/dist/cli/index.mjs +16 -1
  9. package/dist/index.d.cts +31 -5
  10. package/dist/index.d.mts +31 -5
  11. package/dist/lsp/index.cjs +1 -1
  12. package/dist/lsp/index.mjs +1 -1
  13. package/dist/unplugin/index.cjs +33 -9
  14. package/dist/unplugin/index.d.cts +7 -5
  15. package/dist/unplugin/index.d.mts +7 -5
  16. package/dist/unplugin/index.mjs +33 -9
  17. package/docs/.vitepress/config.ts +109 -0
  18. package/docs/.vitepress/theme/custom.css +150 -0
  19. package/docs/.vitepress/theme/index.ts +17 -0
  20. package/docs/api/configuration.md +274 -0
  21. package/docs/api/functions.md +291 -0
  22. package/docs/api/types.md +158 -0
  23. package/docs/guide/basic-usage.md +267 -0
  24. package/docs/guide/cli.md +174 -0
  25. package/docs/guide/generated-code.md +284 -0
  26. package/docs/guide/getting-started.md +171 -0
  27. package/docs/guide/ide-plugin.md +203 -0
  28. package/docs/guide/injection-types.md +287 -0
  29. package/docs/guide/legacy-migration.md +333 -0
  30. package/docs/guide/lifecycle.md +223 -0
  31. package/docs/guide/parent-container.md +321 -0
  32. package/docs/guide/scoped-injections.md +271 -0
  33. package/docs/guide/what-is-neo-syringe.md +162 -0
  34. package/docs/guide/why-neo-syringe.md +219 -0
  35. package/docs/index.md +138 -0
  36. package/docs/public/logo.png +0 -0
  37. package/package.json +15 -12
  38. package/src/analyzer/Analyzer.ts +20 -10
  39. package/src/analyzer/types.ts +55 -49
  40. package/src/cli/index.ts +15 -0
  41. package/src/generator/Generator.ts +24 -2
  42. package/src/generator/GraphValidator.ts +6 -2
  43. package/src/types.ts +30 -4
  44. package/src/unplugin/index.ts +13 -41
  45. package/tests/analyzer/Analyzer.test.ts +4 -4
  46. package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
  47. package/tests/analyzer/Factory.test.ts +2 -2
  48. package/tests/analyzer/Scoped.test.ts +434 -0
  49. package/tests/cli/cli.test.ts +91 -0
  50. package/tests/e2e/container-integration.test.ts +21 -21
  51. package/tests/e2e/generated-code.test.ts +7 -7
  52. package/tests/e2e/scoped.test.ts +370 -0
  53. package/tests/e2e/snapshots.test.ts +2 -2
  54. package/tests/e2e/standalone.test.ts +2 -2
  55. package/tests/generator/ExternalGenerator.test.ts +1 -1
  56. package/tests/generator/FactoryGenerator.test.ts +6 -6
  57. package/tests/generator/Generator.test.ts +2 -2
  58. package/tests/generator/GeneratorDeclarative.test.ts +1 -1
  59. package/tests/generator/GraphValidator.test.ts +1 -1
  60. package/tsconfig.json +2 -1
  61. package/typedoc.json +0 -5
@@ -0,0 +1,162 @@
1
+ # What is Neo-Syringe?
2
+
3
+ Neo-Syringe is a **next-generation dependency injection system** for TypeScript that shifts resolution from **runtime** to **build-time**.
4
+
5
+ ## The Problem with Traditional DI
6
+
7
+ Traditional DI containers (InversifyJS, tsyringe, Awilix) all work the same way:
8
+
9
+ ```typescript
10
+ // Traditional approach
11
+ @injectable()
12
+ class UserService {
13
+ constructor(@inject(TYPES.ILogger) private logger: ILogger) {}
14
+ }
15
+
16
+ // At runtime
17
+ container.resolve(UserService); // ← Resolution happens HERE
18
+ ```
19
+
20
+ This approach has several drawbacks:
21
+
22
+ | Issue | Impact |
23
+ |-------|--------|
24
+ | **Runtime overhead** | DI container code shipped to production |
25
+ | **Reflection required** | Need `reflect-metadata` and decorators |
26
+ | **Interface erasure** | Must use Symbols or string tokens manually |
27
+ | **Late errors** | Missing bindings only discovered at runtime |
28
+ | **Framework coupling** | Classes polluted with DI decorators |
29
+
30
+ ## The Neo-Syringe Solution
31
+
32
+ Neo-Syringe works as a **compiler plugin** that analyzes your configuration and generates optimized code:
33
+
34
+ ```typescript
35
+ // Your code (pure TypeScript!)
36
+ interface ILogger {
37
+ log(msg: string): void;
38
+ }
39
+
40
+ class UserService {
41
+ constructor(private logger: ILogger) {}
42
+ }
43
+
44
+ // Configuration
45
+ export const container = defineBuilderConfig({
46
+ injections: [
47
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
48
+ { token: UserService }
49
+ ]
50
+ });
51
+ ```
52
+
53
+ At build time, this becomes:
54
+
55
+ ```typescript
56
+ // Generated code (no DI library!)
57
+ function create_UserService(container) {
58
+ return new UserService(container.resolve("ILogger"));
59
+ }
60
+
61
+ class NeoContainer {
62
+ resolve(token) {
63
+ if (token === "ILogger") return new ConsoleLogger();
64
+ if (token === UserService) return create_UserService(this);
65
+ }
66
+ }
67
+
68
+ export const container = new NeoContainer();
69
+ ```
70
+
71
+ ## Key Advantages
72
+
73
+ ### 🚀 Zero Runtime Overhead
74
+
75
+ No DI library shipped to production. Just pure factory functions that create instances directly.
76
+
77
+ ### ✨ Native Interface Support
78
+
79
+ Use `useInterface<ILogger>()` instead of managing Symbols. The compiler generates unique IDs automatically.
80
+
81
+ ### 🛡️ Compile-Time Safety
82
+
83
+ Errors are detected in your IDE before you even save the file:
84
+
85
+ - Circular dependencies
86
+ - Missing bindings
87
+ - Duplicate registrations
88
+ - Type mismatches
89
+
90
+ ### 📦 Pure Classes
91
+
92
+ Your business classes have **zero DI dependencies**:
93
+
94
+ ```typescript
95
+ // ✅ Pure TypeScript class
96
+ class UserService {
97
+ constructor(private logger: ILogger) {}
98
+ }
99
+
100
+ // ❌ Traditional approach (polluted)
101
+ @injectable()
102
+ class UserService {
103
+ constructor(@inject(TYPES.ILogger) private logger: ILogger) {}
104
+ }
105
+ ```
106
+
107
+ ### 🔄 Gradual Migration
108
+
109
+ Bridge existing containers while migrating:
110
+
111
+ ```typescript
112
+ export const container = defineBuilderConfig({
113
+ useContainer: legacyTsyringeContainer, // ← Delegate to legacy
114
+ injections: [
115
+ { token: NewService } // New services in Neo-Syringe
116
+ ]
117
+ });
118
+ ```
119
+
120
+ ## How It Works
121
+
122
+ ```
123
+ ┌─────────────────────────────────────────────────────────────┐
124
+ │ BUILD TIME │
125
+ ├─────────────────────────────────────────────────────────────┤
126
+ │ │
127
+ │ 1. defineBuilderConfig({...}) │
128
+ │ │ │
129
+ │ ▼ │
130
+ │ 2. TypeScript Plugin analyzes configuration │
131
+ │ │ │
132
+ │ ▼ │
133
+ │ 3. Generates optimized NeoContainer class │
134
+ │ │ │
135
+ │ ▼ │
136
+ │ 4. Replaces defineBuilderConfig with generated code │
137
+ │ │
138
+ └─────────────────────────────────────────────────────────────┘
139
+
140
+
141
+ ┌─────────────────────────────────────────────────────────────┐
142
+ │ RUNTIME │
143
+ ├─────────────────────────────────────────────────────────────┤
144
+ │ │
145
+ │ container.resolve(UserService) │
146
+ │ │ │
147
+ │ ▼ │
148
+ │ Direct new UserService(new ConsoleLogger()) │
149
+ │ │
150
+ │ ✅ No reflection │
151
+ │ ✅ No container lookup │
152
+ │ ✅ Just function calls │
153
+ │ │
154
+ └─────────────────────────────────────────────────────────────┘
155
+ ```
156
+
157
+ ## Next Steps
158
+
159
+ - [Getting Started](/guide/getting-started) - Install and configure Neo-Syringe
160
+ - [Basic Usage](/guide/basic-usage) - Learn the core concepts
161
+ - [Why Neo-Syringe?](/guide/why-neo-syringe) - Detailed comparison with alternatives
162
+
@@ -0,0 +1,219 @@
1
+ # Why Neo-Syringe?
2
+
3
+ A detailed comparison with other dependency injection solutions.
4
+
5
+ ## Comparison Table
6
+
7
+ | Feature | Neo-Syringe | tsyringe | InversifyJS | Awilix |
8
+ |---------|:-----------:|:--------:|:-----------:|:------:|
9
+ | **Zero runtime overhead** | ✅ | ❌ | ❌ | ❌ |
10
+ | **No decorators needed** | ✅ | ❌ | ❌ | ✅ |
11
+ | **No reflect-metadata** | ✅ | ❌ | ❌ | ✅ |
12
+ | **Interface as tokens** | ✅ | ❌ | ❌ | ❌ |
13
+ | **Compile-time validation** | ✅ | ❌ | ❌ | ❌ |
14
+ | **IDE error detection** | ✅ | ❌ | ❌ | ❌ |
15
+ | **Tree-shakeable** | ✅ | ❌ | ❌ | ❌ |
16
+ | **Works in Edge/Workers** | ✅ | ⚠️ | ⚠️ | ✅ |
17
+
18
+ ## The Cost of Runtime DI
19
+
20
+ Traditional DI containers add significant overhead:
21
+
22
+ ### Bundle Size
23
+
24
+ | Library | Min+Gzip |
25
+ |---------|----------|
26
+ | InversifyJS | ~11 KB |
27
+ | tsyringe | ~4 KB |
28
+ | Awilix | ~8 KB |
29
+ | **Neo-Syringe** | **~0 KB** (generated) |
30
+
31
+ ### Runtime Performance
32
+
33
+ ```typescript
34
+ // Traditional (tsyringe) - Runtime resolution
35
+ container.resolve(UserService);
36
+ // 1. Look up token in registry
37
+ // 2. Check if singleton exists
38
+ // 3. Resolve all dependencies recursively
39
+ // 4. Create instance with reflection
40
+ // 5. Store singleton reference
41
+
42
+ // Neo-Syringe - Direct instantiation
43
+ container.resolve(UserService);
44
+ // 1. Call generated factory function
45
+ // That's it!
46
+ ```
47
+
48
+ ## Code Quality Comparison
49
+
50
+ ### Traditional Approach (tsyringe)
51
+
52
+ ```typescript
53
+ import 'reflect-metadata';
54
+ import { injectable, inject } from 'tsyringe';
55
+
56
+ // Must define symbols manually
57
+ const TYPES = {
58
+ ILogger: Symbol.for('ILogger'),
59
+ IDatabase: Symbol.for('IDatabase'),
60
+ };
61
+
62
+ // Classes polluted with decorators
63
+ @injectable()
64
+ class UserService {
65
+ constructor(
66
+ @inject(TYPES.ILogger) private logger: ILogger,
67
+ @inject(TYPES.IDatabase) private db: IDatabase
68
+ ) {}
69
+ }
70
+
71
+ // Registration
72
+ container.register(TYPES.ILogger, { useClass: ConsoleLogger });
73
+ container.register(TYPES.IDatabase, { useClass: PostgresDatabase });
74
+ container.register(UserService, { useClass: UserService });
75
+ ```
76
+
77
+ ### Neo-Syringe Approach
78
+
79
+ ```typescript
80
+ import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
81
+
82
+ // Pure class - no DI imports!
83
+ class UserService {
84
+ constructor(
85
+ private logger: ILogger,
86
+ private db: IDatabase
87
+ ) {}
88
+ }
89
+
90
+ // Clean configuration
91
+ export const container = defineBuilderConfig({
92
+ injections: [
93
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
94
+ { token: useInterface<IDatabase>(), provider: PostgresDatabase },
95
+ { token: UserService }
96
+ ]
97
+ });
98
+ ```
99
+
100
+ ## Error Detection
101
+
102
+ ### Traditional: Runtime Errors
103
+
104
+ ```typescript
105
+ // tsyringe - Error at RUNTIME
106
+ container.resolve(UserService);
107
+ // Error: Attempted to resolve unregistered dependency token: "ILogger"
108
+ // 💥 App crashes in production!
109
+ ```
110
+
111
+ ### Neo-Syringe: Compile-Time Errors
112
+
113
+ ```typescript
114
+ // Neo-Syringe - Error in IDE instantly
115
+ export const container = defineBuilderConfig({
116
+ injections: [
117
+ { token: UserService } // UserService needs ILogger
118
+ ]
119
+ });
120
+ // 🔴 [Neo-Syringe] Missing binding: 'UserService' depends on 'ILogger'
121
+ // ✅ Fixed before you even save the file!
122
+ ```
123
+
124
+ ## Testing
125
+
126
+ ### Traditional: Complex Mocking (tsyringe example)
127
+
128
+ ```typescript
129
+ // ❌ tsyringe requires manual container reset and re-registration
130
+ import { container } from 'tsyringe';
131
+
132
+ beforeEach(() => {
133
+ container.reset(); // tsyringe API, NOT Neo-Syringe!
134
+ container.register(TYPES.ILogger, { useClass: MockLogger });
135
+ container.register(TYPES.IDatabase, { useClass: MockDatabase });
136
+ container.register(UserService, { useClass: UserService });
137
+ });
138
+ ```
139
+
140
+ ### Neo-Syringe: Natural Overrides
141
+
142
+ ```typescript
143
+ // ✅ Neo-Syringe - Create a test container that overrides production services
144
+ import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
145
+ import { productionContainer } from './container';
146
+
147
+ // Test container inherits from production but overrides specific services
148
+ const testContainer = defineBuilderConfig({
149
+ useContainer: productionContainer,
150
+ injections: [
151
+ { token: useInterface<ILogger>(), provider: MockLogger, scoped: true },
152
+ { token: useInterface<IDatabase>(), provider: MockDatabase, scoped: true }
153
+ ]
154
+ });
155
+
156
+ // No reset needed - each test file can have its own container!
157
+ const userService = testContainer.resolve(UserService);
158
+ ```
159
+
160
+ ## Edge Computing / Workers
161
+
162
+ Traditional DI often fails in edge environments:
163
+
164
+ ```typescript
165
+ // ❌ tsyringe in Cloudflare Workers
166
+ // Error: reflect-metadata requires global Reflect object
167
+
168
+ // ❌ InversifyJS in Vercel Edge
169
+ // Error: Cannot use decorators in Edge Runtime
170
+ ```
171
+
172
+ Neo-Syringe works everywhere:
173
+
174
+ ```typescript
175
+ // ✅ Neo-Syringe in any environment
176
+ // Generated code is pure JavaScript
177
+ export class NeoContainer {
178
+ resolve(token) {
179
+ if (token === "ILogger") return new ConsoleLogger();
180
+ // ... pure function calls
181
+ }
182
+ }
183
+ ```
184
+
185
+ ## Migration Path
186
+
187
+ You don't have to migrate everything at once:
188
+
189
+ ```typescript
190
+ // Bridge legacy container
191
+ import { legacyContainer } from './legacy-tsyringe';
192
+ import { declareContainerTokens } from '@djodjonx/neo-syringe';
193
+
194
+ const legacy = declareContainerTokens<{
195
+ AuthService: AuthService;
196
+ UserRepository: UserRepository;
197
+ }>(legacyContainer);
198
+
199
+ // New services use Neo-Syringe
200
+ export const container = defineBuilderConfig({
201
+ useContainer: legacy, // Delegate to legacy
202
+ injections: [
203
+ { token: NewService },
204
+ { token: AnotherNewService }
205
+ ]
206
+ });
207
+ ```
208
+
209
+ ## Summary
210
+
211
+ | Aspect | Traditional DI | Neo-Syringe |
212
+ |--------|---------------|-------------|
213
+ | **When errors occur** | Runtime | Compile-time |
214
+ | **Bundle impact** | 4-11 KB | 0 KB |
215
+ | **Class purity** | Polluted with decorators | 100% pure |
216
+ | **Interface support** | Manual Symbols | Automatic |
217
+ | **Edge compatibility** | Limited | Full |
218
+ | **Performance** | Map lookups + reflection | Direct calls |
219
+
package/docs/index.md ADDED
@@ -0,0 +1,138 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: Neo-Syringe
6
+ text: Compile-Time DI
7
+ tagline: Zero-overhead dependency injection that shifts resolution from Runtime to Build-Time. No reflection, no decorators, just pure TypeScript.
8
+ image:
9
+ src: /logo.png
10
+ alt: Neo-Syringe
11
+ actions:
12
+ - theme: brand
13
+ text: Get Started
14
+ link: /guide/getting-started
15
+ - theme: alt
16
+ text: View on GitHub
17
+ link: https://github.com/djodjonx/neo-syringe
18
+
19
+ features:
20
+ - icon: ✨
21
+ title: Interface as Tokens
22
+ details: Native support for useInterface<ILogger>() without manual Symbols. TypeScript interfaces work seamlessly.
23
+
24
+ - icon: 🚀
25
+ title: Zero Runtime Overhead
26
+ details: No reflection, no reflect-metadata. Just pure factory functions generated at build time.
27
+
28
+ - icon: 🛡️
29
+ title: Compile-Time Safety
30
+ details: Detect circular dependencies, missing bindings, and type mismatches instantly in your IDE.
31
+
32
+ - icon: 🔄
33
+ title: Gradual Migration
34
+ details: Bridge existing containers like tsyringe or InversifyJS with useContainer while migrating.
35
+
36
+ - icon: 📦
37
+ title: Pure Classes
38
+ details: Your business classes stay 100% pure - no decorators, no DI imports, no framework coupling.
39
+
40
+ - icon: 🤖
41
+ title: CI Validation
42
+ details: Standalone CLI to verify your dependency graph before deployment.
43
+ ---
44
+
45
+ <style>
46
+ :root {
47
+ --vp-home-hero-name-color: transparent;
48
+ --vp-home-hero-name-background: linear-gradient(135deg, #0d9488 0%, #f97316 100%);
49
+ }
50
+ </style>
51
+
52
+ ## Why Choose Neo-Syringe?
53
+
54
+ Traditional DI containers like InversifyJS and tsyringe rely on **runtime resolution**:
55
+
56
+ - ❌ Ship DI container logic to the browser
57
+ - ❌ Errors happen at runtime
58
+ - ❌ Interfaces are erased, requiring manual Symbols
59
+ - ❌ Need decorators and reflect-metadata
60
+
61
+ **Neo-Syringe is different.** It works as a **compiler plugin**:
62
+
63
+ - ✅ Generate optimized factories at build time
64
+ - ✅ Errors detected in your IDE
65
+ - ✅ Automatic interface IDs
66
+ - ✅ Pure TypeScript, no decorators
67
+
68
+ ## Quick Example
69
+
70
+ ```typescript
71
+ // Pure TypeScript - no decorators!
72
+ interface ILogger {
73
+ log(msg: string): void;
74
+ }
75
+
76
+ class ConsoleLogger implements ILogger {
77
+ log(msg: string) { console.log(msg); }
78
+ }
79
+
80
+ class UserService {
81
+ constructor(private logger: ILogger) {}
82
+ }
83
+
84
+ // Configure the container
85
+ import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
86
+
87
+ export const container = defineBuilderConfig({
88
+ name: 'AppContainer',
89
+ injections: [
90
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
91
+ { token: UserService }
92
+ ]
93
+ });
94
+
95
+ // Use it
96
+ const userService = container.resolve(UserService);
97
+ ```
98
+
99
+ ## Generated Output
100
+
101
+ The build plugin transforms your configuration into optimized code:
102
+
103
+ ```typescript
104
+ // Generated at build time
105
+ function create_ILogger() {
106
+ return new ConsoleLogger();
107
+ }
108
+
109
+ function create_UserService(container) {
110
+ return new UserService(container.resolve("ILogger"));
111
+ }
112
+
113
+ export class NeoContainer {
114
+ resolve(token) {
115
+ if (token === "ILogger") return this.getInstance("ILogger", create_ILogger);
116
+ if (token === UserService) return this.getInstance(UserService, create_UserService);
117
+ throw new Error(`Service not found: ${token}`);
118
+ }
119
+ }
120
+ ```
121
+
122
+ **Zero DI library shipped to production!**
123
+
124
+ <div style="text-align: center; margin-top: 3rem;">
125
+ <a href="./guide/getting-started" style="
126
+ display: inline-block;
127
+ padding: 12px 24px;
128
+ background: linear-gradient(135deg, #0d9488 0%, #14b8a6 100%);
129
+ color: white;
130
+ text-decoration: none;
131
+ border-radius: 8px;
132
+ font-weight: 600;
133
+ transition: transform 0.2s;
134
+ ">
135
+ Get Started →
136
+ </a>
137
+ </div>
138
+
Binary file
package/package.json CHANGED
@@ -1,29 +1,30 @@
1
1
  {
2
2
  "name": "@djodjonx/neo-syringe",
3
- "version": "1.1.5",
3
+ "version": "1.2.2",
4
4
  "description": "Zero-Overhead, Compile-Time Dependency Injection for TypeScript",
5
5
  "type": "module",
6
- "main": "dist/index.js",
6
+ "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
8
- "types": "dist/index.d.ts",
8
+ "types": "dist/index.d.mts",
9
9
  "exports": {
10
10
  ".": {
11
- "types": "./dist/index.d.ts",
11
+ "types": "./dist/index.d.mts",
12
12
  "import": "./dist/index.mjs",
13
- "require": "./dist/index.js"
13
+ "require": "./dist/index.cjs"
14
14
  },
15
15
  "./plugin": {
16
- "types": "./dist/unplugin/index.d.ts",
16
+ "types": "./dist/unplugin/index.d.mts",
17
17
  "import": "./dist/unplugin/index.mjs",
18
- "require": "./dist/unplugin/index.js"
18
+ "require": "./dist/unplugin/index.cjs"
19
19
  },
20
20
  "./lsp": {
21
- "types": "./dist/lsp/index.d.ts",
22
- "require": "./dist/lsp/index.js"
21
+ "types": "./dist/lsp/index.d.mts",
22
+ "import": "./dist/lsp/index.mjs",
23
+ "require": "./dist/lsp/index.cjs"
23
24
  }
24
25
  },
25
26
  "bin": {
26
- "neo-syringe": "./dist/cli/index.js"
27
+ "neo-syringe": "./dist/cli/index.mjs"
27
28
  },
28
29
  "scripts": {
29
30
  "build": "tsdown",
@@ -35,7 +36,9 @@
35
36
  "validate": "pnpm lint && pnpm typecheck && pnpm test",
36
37
  "prebuild": "pnpm validate",
37
38
  "prepublishOnly": "pnpm build",
38
- "docs": "typedoc",
39
+ "docs:dev": "vitepress dev docs",
40
+ "docs:build": "vitepress build docs",
41
+ "docs:preview": "vitepress preview docs",
39
42
  "release": "pnpm validate && standard-version",
40
43
  "release:dry": "standard-version --dry-run",
41
44
  "prepare": "husky"
@@ -65,9 +68,9 @@
65
68
  "standard-version": "^9.5.0",
66
69
  "tsdown": "0.20.0-beta.3",
67
70
  "tsyringe": "^4.10.0",
68
- "typedoc": "^0.28.16",
69
71
  "typescript": "^5.9.3",
70
72
  "unplugin": "^2.3.11",
73
+ "vitepress": "^1.6.4",
71
74
  "vitest": "^4.0.17"
72
75
  }
73
76
  }
@@ -283,11 +283,12 @@ export class Analyzer {
283
283
  }
284
284
 
285
285
  private parseInjectionObject(obj: ts.ObjectLiteralExpression, graph: DependencyGraph): void {
286
- // Extract properties: token, provider, scope, useFactory
286
+ // Extract properties: token, provider, lifecycle, useFactory, scoped
287
287
  let tokenNode: ts.Expression | undefined;
288
288
  let providerNode: ts.Expression | undefined;
289
- let scope: 'singleton' | 'transient' = 'singleton';
289
+ let lifecycle: 'singleton' | 'transient' = 'singleton';
290
290
  let useFactory = false;
291
+ let isScoped = false;
291
292
 
292
293
  for (const prop of obj.properties) {
293
294
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
@@ -296,13 +297,18 @@ export class Analyzer {
296
297
  tokenNode = prop.initializer;
297
298
  } else if (prop.name.text === 'provider') {
298
299
  providerNode = prop.initializer;
299
- } else if (prop.name.text === 'scope' && ts.isStringLiteral(prop.initializer)) {
300
- if (prop.initializer.text === 'transient') scope = 'transient';
300
+ } else if (prop.name.text === 'lifecycle' && ts.isStringLiteral(prop.initializer)) {
301
+ if (prop.initializer.text === 'transient') lifecycle = 'transient';
301
302
  } else if (prop.name.text === 'useFactory') {
302
303
  // Check if useFactory: true
303
304
  if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
304
305
  useFactory = true;
305
306
  }
307
+ } else if (prop.name.text === 'scoped') {
308
+ // Check if scoped: true
309
+ if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
310
+ isScoped = true;
311
+ }
306
312
  }
307
313
  }
308
314
 
@@ -362,7 +368,8 @@ export class Analyzer {
362
368
  type = 'factory';
363
369
 
364
370
  if (tokenId) {
365
- if (graph.nodes.has(tokenId)) {
371
+ // Check for duplicate - allow if scoped: true (intentional override)
372
+ if (graph.nodes.has(tokenId) && !isScoped) {
366
373
  throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
367
374
  }
368
375
 
@@ -371,11 +378,12 @@ export class Analyzer {
371
378
  tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : undefined,
372
379
  registrationNode: obj,
373
380
  type: 'factory',
374
- scope: scope,
381
+ lifecycle: lifecycle,
375
382
  isInterfaceToken,
376
383
  isValueToken,
377
384
  isFactory: true,
378
- factorySource
385
+ factorySource,
386
+ isScoped
379
387
  };
380
388
  graph.nodes.set(tokenId, { service: definition, dependencies: [] });
381
389
  }
@@ -406,7 +414,8 @@ export class Analyzer {
406
414
  }
407
415
 
408
416
  if (tokenId && implementationSymbol) {
409
- if (graph.nodes.has(tokenId)) {
417
+ // Check for duplicate - allow if scoped: true (intentional override)
418
+ if (graph.nodes.has(tokenId) && !isScoped) {
410
419
  throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
411
420
  }
412
421
 
@@ -416,8 +425,9 @@ export class Analyzer {
416
425
  tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : undefined,
417
426
  registrationNode: obj,
418
427
  type: type,
419
- scope: scope,
420
- isInterfaceToken: isInterfaceToken || (ts.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode))
428
+ lifecycle: lifecycle,
429
+ isInterfaceToken: isInterfaceToken || (ts.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode)),
430
+ isScoped
421
431
  };
422
432
  graph.nodes.set(tokenId, { service: definition, dependencies: [] });
423
433
  }