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