@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.
- package/.github/workflows/ci.yml +6 -5
- package/.github/workflows/docs.yml +59 -0
- package/CHANGELOG.md +27 -0
- package/README.md +74 -740
- package/dist/{GraphValidator-G0F4QiLk.cjs → GraphValidator-CV4VoJl0.cjs} +18 -10
- package/dist/{GraphValidator-C8ldJtNp.mjs → GraphValidator-DXqqkNdS.mjs} +18 -10
- package/dist/cli/index.cjs +16 -1
- package/dist/cli/index.mjs +16 -1
- package/dist/index.d.cts +31 -5
- package/dist/index.d.mts +31 -5
- package/dist/lsp/index.cjs +1 -1
- package/dist/lsp/index.mjs +1 -1
- package/dist/unplugin/index.cjs +33 -9
- package/dist/unplugin/index.d.cts +7 -5
- package/dist/unplugin/index.d.mts +7 -5
- package/dist/unplugin/index.mjs +33 -9
- package/docs/.vitepress/config.ts +109 -0
- package/docs/.vitepress/theme/custom.css +150 -0
- package/docs/.vitepress/theme/index.ts +17 -0
- package/docs/api/configuration.md +274 -0
- package/docs/api/functions.md +291 -0
- package/docs/api/types.md +158 -0
- package/docs/guide/basic-usage.md +267 -0
- package/docs/guide/cli.md +174 -0
- package/docs/guide/generated-code.md +284 -0
- package/docs/guide/getting-started.md +171 -0
- package/docs/guide/ide-plugin.md +203 -0
- package/docs/guide/injection-types.md +287 -0
- package/docs/guide/legacy-migration.md +333 -0
- package/docs/guide/lifecycle.md +223 -0
- package/docs/guide/parent-container.md +321 -0
- package/docs/guide/scoped-injections.md +271 -0
- package/docs/guide/what-is-neo-syringe.md +162 -0
- package/docs/guide/why-neo-syringe.md +219 -0
- package/docs/index.md +138 -0
- package/docs/public/logo.png +0 -0
- package/package.json +15 -12
- package/src/analyzer/Analyzer.ts +20 -10
- package/src/analyzer/types.ts +55 -49
- package/src/cli/index.ts +15 -0
- package/src/generator/Generator.ts +24 -2
- package/src/generator/GraphValidator.ts +6 -2
- package/src/types.ts +30 -4
- package/src/unplugin/index.ts +13 -41
- package/tests/analyzer/Analyzer.test.ts +4 -4
- package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
- package/tests/analyzer/Factory.test.ts +2 -2
- package/tests/analyzer/Scoped.test.ts +434 -0
- package/tests/cli/cli.test.ts +91 -0
- package/tests/e2e/container-integration.test.ts +21 -21
- package/tests/e2e/generated-code.test.ts +7 -7
- package/tests/e2e/scoped.test.ts +370 -0
- package/tests/e2e/snapshots.test.ts +2 -2
- package/tests/e2e/standalone.test.ts +2 -2
- package/tests/generator/ExternalGenerator.test.ts +1 -1
- package/tests/generator/FactoryGenerator.test.ts +6 -6
- package/tests/generator/Generator.test.ts +2 -2
- package/tests/generator/GeneratorDeclarative.test.ts +1 -1
- package/tests/generator/GraphValidator.test.ts +1 -1
- package/tsconfig.json +2 -1
- 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.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Zero-Overhead, Compile-Time Dependency Injection for TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
8
|
-
"types": "dist/index.d.
|
|
8
|
+
"types": "dist/index.d.mts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"types": "./dist/index.d.
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
12
|
"import": "./dist/index.mjs",
|
|
13
|
-
"require": "./dist/index.
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
14
|
},
|
|
15
15
|
"./plugin": {
|
|
16
|
-
"types": "./dist/unplugin/index.d.
|
|
16
|
+
"types": "./dist/unplugin/index.d.mts",
|
|
17
17
|
"import": "./dist/unplugin/index.mjs",
|
|
18
|
-
"require": "./dist/unplugin/index.
|
|
18
|
+
"require": "./dist/unplugin/index.cjs"
|
|
19
19
|
},
|
|
20
20
|
"./lsp": {
|
|
21
|
-
"types": "./dist/lsp/index.d.
|
|
22
|
-
"
|
|
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.
|
|
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": "
|
|
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
|
}
|
package/src/analyzer/Analyzer.ts
CHANGED
|
@@ -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,
|
|
286
|
+
// Extract properties: token, provider, lifecycle, useFactory, scoped
|
|
287
287
|
let tokenNode: ts.Expression | undefined;
|
|
288
288
|
let providerNode: ts.Expression | undefined;
|
|
289
|
-
let
|
|
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 === '
|
|
300
|
-
if (prop.initializer.text === '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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
}
|