@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.
- package/.github/workflows/docs.yml +59 -0
- package/CHANGELOG.md +14 -0
- package/README.md +72 -779
- package/dist/cli/index.cjs +15 -0
- package/dist/cli/index.mjs +15 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/unplugin/index.cjs +31 -7
- package/dist/unplugin/index.d.cts +7 -5
- package/dist/unplugin/index.d.mts +7 -5
- package/dist/unplugin/index.mjs +31 -7
- 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 +5 -3
- package/src/analyzer/types.ts +52 -52
- package/src/cli/index.ts +15 -0
- package/src/generator/Generator.ts +23 -1
- package/src/types.ts +1 -1
- package/src/unplugin/index.ts +13 -41
- package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
- package/tests/e2e/container-integration.test.ts +19 -19
- package/tests/e2e/generated-code.test.ts +2 -2
- package/tsconfig.json +2 -1
- package/typedoc.json +0 -5
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Basic Usage
|
|
2
|
+
|
|
3
|
+
Learn the fundamental concepts and injection patterns in Neo-Syringe.
|
|
4
|
+
|
|
5
|
+
## Container Configuration
|
|
6
|
+
|
|
7
|
+
All configuration is done through `defineBuilderConfig`:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { defineBuilderConfig } from '@djodjonx/neo-syringe';
|
|
11
|
+
|
|
12
|
+
export const container = defineBuilderConfig({
|
|
13
|
+
name: 'MyContainer', // Optional: container name for debugging
|
|
14
|
+
injections: [ // Required: list of injections
|
|
15
|
+
// ... your injections
|
|
16
|
+
],
|
|
17
|
+
extends: [], // Optional: inherit from partials
|
|
18
|
+
useContainer: undefined // Optional: parent container
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Injection Types
|
|
23
|
+
|
|
24
|
+
### Class Token (Autowire)
|
|
25
|
+
|
|
26
|
+
The simplest form - register a class and let Neo-Syringe resolve its dependencies:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
class UserRepository {
|
|
30
|
+
findAll() { return []; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class UserService {
|
|
34
|
+
constructor(private repo: UserRepository) {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const container = defineBuilderConfig({
|
|
38
|
+
injections: [
|
|
39
|
+
{ token: UserRepository },
|
|
40
|
+
{ token: UserService } // Dependencies auto-resolved
|
|
41
|
+
]
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Interface Token
|
|
46
|
+
|
|
47
|
+
Use `useInterface<T>()` to bind interfaces to implementations:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { useInterface } from '@djodjonx/neo-syringe';
|
|
51
|
+
|
|
52
|
+
interface ILogger {
|
|
53
|
+
log(msg: string): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class ConsoleLogger implements ILogger {
|
|
57
|
+
log(msg: string) { console.log(msg); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class FileLogger implements ILogger {
|
|
61
|
+
log(msg: string) { /* write to file */ }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const container = defineBuilderConfig({
|
|
65
|
+
injections: [
|
|
66
|
+
{ token: useInterface<ILogger>(), provider: ConsoleLogger }
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Resolve by interface
|
|
71
|
+
const logger = container.resolve(useInterface<ILogger>());
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
::: tip Automatic ID Generation
|
|
75
|
+
`useInterface<ILogger>()` generates a unique string ID at compile-time. You don't need to manage Symbols manually!
|
|
76
|
+
:::
|
|
77
|
+
|
|
78
|
+
### Explicit Provider
|
|
79
|
+
|
|
80
|
+
Override the default implementation:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
class UserService {
|
|
84
|
+
// ...
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class MockUserService extends UserService {
|
|
88
|
+
// Test implementation
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const container = defineBuilderConfig({
|
|
92
|
+
injections: [
|
|
93
|
+
{ token: UserService, provider: MockUserService }
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Factory Provider
|
|
99
|
+
|
|
100
|
+
Use factory functions for dynamic instantiation:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Arrow functions are auto-detected as factories
|
|
104
|
+
{
|
|
105
|
+
token: useInterface<IConfig>(),
|
|
106
|
+
provider: (container) => ({
|
|
107
|
+
apiUrl: process.env.API_URL ?? 'http://localhost',
|
|
108
|
+
timeout: 5000
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Factory with dependencies
|
|
113
|
+
{
|
|
114
|
+
token: useInterface<IService>(),
|
|
115
|
+
provider: (container) => {
|
|
116
|
+
const logger = container.resolve(useInterface<ILogger>());
|
|
117
|
+
const config = container.resolve(useInterface<IConfig>());
|
|
118
|
+
return new MyService(logger, config);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Explicit factory flag (for non-arrow functions)
|
|
123
|
+
{
|
|
124
|
+
token: useInterface<IDatabase>(),
|
|
125
|
+
provider: createDatabaseConnection,
|
|
126
|
+
useFactory: true
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Property Token
|
|
131
|
+
|
|
132
|
+
Inject primitive values (string, number, boolean) while keeping classes pure:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { useProperty } from '@djodjonx/neo-syringe';
|
|
136
|
+
|
|
137
|
+
// Pure class - no DI imports!
|
|
138
|
+
class ApiService {
|
|
139
|
+
constructor(
|
|
140
|
+
private apiUrl: string,
|
|
141
|
+
private timeout: number,
|
|
142
|
+
private debug: boolean
|
|
143
|
+
) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Define property tokens
|
|
147
|
+
const apiUrl = useProperty<string>(ApiService, 'apiUrl');
|
|
148
|
+
const timeout = useProperty<number>(ApiService, 'timeout');
|
|
149
|
+
const debug = useProperty<boolean>(ApiService, 'debug');
|
|
150
|
+
|
|
151
|
+
export const container = defineBuilderConfig({
|
|
152
|
+
injections: [
|
|
153
|
+
{ token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
|
|
154
|
+
{ token: timeout, provider: () => 5000 },
|
|
155
|
+
{ token: debug, provider: () => process.env.NODE_ENV === 'development' },
|
|
156
|
+
{ token: ApiService } // Primitives auto-wired!
|
|
157
|
+
]
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
::: info Type Safety
|
|
162
|
+
`useProperty(ApiService, 'apiUrl')` creates a unique token scoped to that specific class parameter. This means `useProperty(ServiceA, 'url')` ≠ `useProperty(ServiceB, 'url')`.
|
|
163
|
+
:::
|
|
164
|
+
|
|
165
|
+
## Resolving Services
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { container } from './container';
|
|
169
|
+
import { UserService } from './services/user.service';
|
|
170
|
+
import { useInterface } from '@djodjonx/neo-syringe';
|
|
171
|
+
import type { ILogger } from './services/logger';
|
|
172
|
+
|
|
173
|
+
// Resolve by class
|
|
174
|
+
const userService = container.resolve(UserService);
|
|
175
|
+
|
|
176
|
+
// Resolve by interface
|
|
177
|
+
const logger = container.resolve(useInterface<ILogger>());
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Partials (Modular Configuration)
|
|
181
|
+
|
|
182
|
+
Split configuration into reusable modules:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// logging.partial.ts
|
|
186
|
+
import { definePartialConfig, useInterface } from '@djodjonx/neo-syringe';
|
|
187
|
+
|
|
188
|
+
export const loggingConfig = definePartialConfig({
|
|
189
|
+
injections: [
|
|
190
|
+
{ token: useInterface<ILogger>(), provider: ConsoleLogger }
|
|
191
|
+
]
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// database.partial.ts
|
|
195
|
+
export const databaseConfig = definePartialConfig({
|
|
196
|
+
injections: [
|
|
197
|
+
{ token: useInterface<IDatabase>(), provider: PostgresDatabase }
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// container.ts
|
|
202
|
+
export const container = defineBuilderConfig({
|
|
203
|
+
extends: [loggingConfig, databaseConfig], // Inherit injections
|
|
204
|
+
injections: [
|
|
205
|
+
{ token: UserService },
|
|
206
|
+
{ token: OrderService }
|
|
207
|
+
]
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Complete Example
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// interfaces.ts
|
|
215
|
+
export interface ILogger {
|
|
216
|
+
log(msg: string): void;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface IUserRepository {
|
|
220
|
+
findById(id: string): User | null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// implementations.ts
|
|
224
|
+
export class ConsoleLogger implements ILogger {
|
|
225
|
+
log(msg: string) { console.log(`[LOG] ${msg}`); }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export class InMemoryUserRepository implements IUserRepository {
|
|
229
|
+
private users = new Map<string, User>();
|
|
230
|
+
|
|
231
|
+
findById(id: string) {
|
|
232
|
+
return this.users.get(id) ?? null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// services.ts
|
|
237
|
+
export class UserService {
|
|
238
|
+
constructor(
|
|
239
|
+
private logger: ILogger,
|
|
240
|
+
private repo: IUserRepository
|
|
241
|
+
) {}
|
|
242
|
+
|
|
243
|
+
getUser(id: string) {
|
|
244
|
+
this.logger.log(`Fetching user ${id}`);
|
|
245
|
+
return this.repo.findById(id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// container.ts
|
|
250
|
+
import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
|
|
251
|
+
|
|
252
|
+
export const container = defineBuilderConfig({
|
|
253
|
+
name: 'AppContainer',
|
|
254
|
+
injections: [
|
|
255
|
+
{ token: useInterface<ILogger>(), provider: ConsoleLogger },
|
|
256
|
+
{ token: useInterface<IUserRepository>(), provider: InMemoryUserRepository },
|
|
257
|
+
{ token: UserService }
|
|
258
|
+
]
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// main.ts
|
|
262
|
+
import { container } from './container';
|
|
263
|
+
|
|
264
|
+
const userService = container.resolve(UserService);
|
|
265
|
+
const user = userService.getUser('123');
|
|
266
|
+
```
|
|
267
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# CLI Validator
|
|
2
|
+
|
|
3
|
+
Validate your dependency graph in CI/CD pipelines.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The CLI is included with the main package:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx neo-syringe
|
|
11
|
+
# or
|
|
12
|
+
pnpm exec neo-syringe
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Run in your project root (where `tsconfig.json` is located):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
neo-syringe
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Options
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
neo-syringe [options]
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
-p, --project <path> Path to tsconfig.json (default: "./tsconfig.json")
|
|
30
|
+
-h, --help Display help
|
|
31
|
+
-v, --version Display version
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Output
|
|
35
|
+
|
|
36
|
+
### Success
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
🔍 Analyzing project: /path/to/tsconfig.json
|
|
40
|
+
Found 45 services.
|
|
41
|
+
🛡️ Validating graph...
|
|
42
|
+
✅ Validation passed! No circular dependencies or missing bindings found.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Circular Dependency Detected
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
🔍 Analyzing project: /path/to/tsconfig.json
|
|
49
|
+
Found 12 services.
|
|
50
|
+
🛡️ Validating graph...
|
|
51
|
+
❌ Validation failed!
|
|
52
|
+
|
|
53
|
+
Error: Circular dependency detected: A -> B -> C -> A
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Missing Binding
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
🔍 Analyzing project: /path/to/tsconfig.json
|
|
60
|
+
Found 8 services.
|
|
61
|
+
🛡️ Validating graph...
|
|
62
|
+
❌ Validation failed!
|
|
63
|
+
|
|
64
|
+
Error: Missing binding: 'UserService' depends on 'ILogger', but no provider registered.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Duplicate Registration
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
🔍 Analyzing project: /path/to/tsconfig.json
|
|
71
|
+
Found 15 services.
|
|
72
|
+
🛡️ Validating graph...
|
|
73
|
+
❌ Validation failed!
|
|
74
|
+
|
|
75
|
+
Error: Duplicate registration: 'ILogger' is already registered in the parent container.
|
|
76
|
+
Use 'scoped: true' to override the parent's registration intentionally.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Exit Codes
|
|
80
|
+
|
|
81
|
+
| Code | Meaning |
|
|
82
|
+
|------|---------|
|
|
83
|
+
| 0 | Validation passed |
|
|
84
|
+
| 1 | Validation failed |
|
|
85
|
+
| 2 | Configuration error |
|
|
86
|
+
|
|
87
|
+
## CI/CD Integration
|
|
88
|
+
|
|
89
|
+
### GitHub Actions
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
# .github/workflows/ci.yml
|
|
93
|
+
name: CI
|
|
94
|
+
|
|
95
|
+
on: [push, pull_request]
|
|
96
|
+
|
|
97
|
+
jobs:
|
|
98
|
+
validate:
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v4
|
|
102
|
+
|
|
103
|
+
- uses: pnpm/action-setup@v4
|
|
104
|
+
|
|
105
|
+
- uses: actions/setup-node@v4
|
|
106
|
+
with:
|
|
107
|
+
node-version: '20'
|
|
108
|
+
cache: 'pnpm'
|
|
109
|
+
|
|
110
|
+
- run: pnpm install --frozen-lockfile
|
|
111
|
+
|
|
112
|
+
- name: Validate DI Graph
|
|
113
|
+
run: pnpm exec neo-syringe
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### GitLab CI
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
# .gitlab-ci.yml
|
|
120
|
+
validate:
|
|
121
|
+
stage: test
|
|
122
|
+
script:
|
|
123
|
+
- pnpm install
|
|
124
|
+
- pnpm exec neo-syringe
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### npm scripts
|
|
128
|
+
|
|
129
|
+
Add to `package.json`:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"scripts": {
|
|
134
|
+
"validate": "neo-syringe",
|
|
135
|
+
"prebuild": "neo-syringe",
|
|
136
|
+
"ci": "pnpm lint && pnpm validate && pnpm test"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Best Practices
|
|
142
|
+
|
|
143
|
+
### Run Before Build
|
|
144
|
+
|
|
145
|
+
Validate before building to catch errors early:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"scripts": {
|
|
150
|
+
"prebuild": "neo-syringe",
|
|
151
|
+
"build": "vite build"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Run in PR Checks
|
|
157
|
+
|
|
158
|
+
Add validation to your PR workflow:
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
- name: Validate Dependencies
|
|
162
|
+
run: pnpm exec neo-syringe
|
|
163
|
+
# Fails the PR if validation fails
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Use with Husky
|
|
167
|
+
|
|
168
|
+
Validate on pre-push:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# .husky/pre-push
|
|
172
|
+
pnpm exec neo-syringe
|
|
173
|
+
```
|
|
174
|
+
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Generated Code
|
|
2
|
+
|
|
3
|
+
Understand what the compiler produces.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Neo-Syringe transforms your configuration into optimized TypeScript at build time. This page shows what your code looks like before and after compilation.
|
|
8
|
+
|
|
9
|
+
## Before (Your Configuration)
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// container.ts
|
|
13
|
+
import { defineBuilderConfig, useInterface, useProperty } from '@djodjonx/neo-syringe';
|
|
14
|
+
|
|
15
|
+
interface ILogger {
|
|
16
|
+
log(msg: string): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class ConsoleLogger implements ILogger {
|
|
20
|
+
log(msg: string) { console.log(msg); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ApiService {
|
|
24
|
+
constructor(
|
|
25
|
+
private logger: ILogger,
|
|
26
|
+
private apiUrl: string
|
|
27
|
+
) {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const apiUrl = useProperty<string>(ApiService, 'apiUrl');
|
|
31
|
+
|
|
32
|
+
export const container = defineBuilderConfig({
|
|
33
|
+
name: 'AppContainer',
|
|
34
|
+
injections: [
|
|
35
|
+
{ token: useInterface<ILogger>(), provider: ConsoleLogger },
|
|
36
|
+
{ token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
|
|
37
|
+
{ token: ApiService }
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## After (Generated Code)
|
|
43
|
+
|
|
44
|
+
The build plugin replaces the entire file:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// container.ts (after build)
|
|
48
|
+
import * as Import_0 from './container';
|
|
49
|
+
|
|
50
|
+
// -- Factories --
|
|
51
|
+
|
|
52
|
+
function create_ILogger(container: NeoContainer) {
|
|
53
|
+
return new Import_0.ConsoleLogger();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function create_ApiService_apiUrl(container: NeoContainer) {
|
|
57
|
+
const userFactory = () => process.env.API_URL ?? 'http://localhost';
|
|
58
|
+
return userFactory(container);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function create_ApiService(container: NeoContainer) {
|
|
62
|
+
return new Import_0.ApiService(
|
|
63
|
+
container.resolve("ILogger"),
|
|
64
|
+
container.resolve("PropertyToken:ApiService.apiUrl")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// -- Container --
|
|
69
|
+
|
|
70
|
+
export class NeoContainer {
|
|
71
|
+
private instances = new Map<any, any>();
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
private parent?: any,
|
|
75
|
+
private legacy?: any[],
|
|
76
|
+
private name: string = 'AppContainer'
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
public resolve(token: any): any {
|
|
80
|
+
// 1. Try local resolution
|
|
81
|
+
const result = this.resolveLocal(token);
|
|
82
|
+
if (result !== undefined) return result;
|
|
83
|
+
|
|
84
|
+
// 2. Delegate to parent
|
|
85
|
+
if (this.parent) {
|
|
86
|
+
try {
|
|
87
|
+
return this.parent.resolve(token);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Continue to legacy
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. Delegate to legacy containers
|
|
94
|
+
if (this.legacy) {
|
|
95
|
+
for (const legacyContainer of this.legacy) {
|
|
96
|
+
try {
|
|
97
|
+
if (legacyContainer.resolve) {
|
|
98
|
+
return legacyContainer.resolve(token);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// Try next
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`[${this.name}] Service not found: ${token}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private resolveLocal(token: any): any {
|
|
110
|
+
// Interface token (string-based)
|
|
111
|
+
if (token === "ILogger") {
|
|
112
|
+
if (!this.instances.has("ILogger")) {
|
|
113
|
+
this.instances.set("ILogger", create_ILogger(this));
|
|
114
|
+
}
|
|
115
|
+
return this.instances.get("ILogger");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Property token (string-based)
|
|
119
|
+
if (token === "PropertyToken:ApiService.apiUrl") {
|
|
120
|
+
if (!this.instances.has("PropertyToken:ApiService.apiUrl")) {
|
|
121
|
+
this.instances.set("PropertyToken:ApiService.apiUrl", create_ApiService_apiUrl(this));
|
|
122
|
+
}
|
|
123
|
+
return this.instances.get("PropertyToken:ApiService.apiUrl");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Class token (reference-based)
|
|
127
|
+
if (token === Import_0.ApiService) {
|
|
128
|
+
if (!this.instances.has(Import_0.ApiService)) {
|
|
129
|
+
this.instances.set(Import_0.ApiService, create_ApiService(this));
|
|
130
|
+
}
|
|
131
|
+
return this.instances.get(Import_0.ApiService);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public createChildContainer(): NeoContainer {
|
|
138
|
+
return new NeoContainer(this, this.legacy, `Child of ${this.name}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// For debugging
|
|
142
|
+
public get _graph() {
|
|
143
|
+
return ["ILogger", "PropertyToken:ApiService.apiUrl", "ApiService"];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const container = new NeoContainer();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Key Observations
|
|
151
|
+
|
|
152
|
+
### Token Resolution
|
|
153
|
+
|
|
154
|
+
| Token Type | Resolution Method | Example |
|
|
155
|
+
|------------|-------------------|---------|
|
|
156
|
+
| Interface | String comparison | `token === "ILogger"` |
|
|
157
|
+
| Class | Reference comparison | `token === Import_0.ApiService` |
|
|
158
|
+
| Property | String comparison | `token === "PropertyToken:ApiService.apiUrl"` |
|
|
159
|
+
|
|
160
|
+
### Singleton Pattern
|
|
161
|
+
|
|
162
|
+
Singletons use the `instances` Map:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
if (!this.instances.has("ILogger")) {
|
|
166
|
+
this.instances.set("ILogger", create_ILogger(this));
|
|
167
|
+
}
|
|
168
|
+
return this.instances.get("ILogger");
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Transient Pattern
|
|
172
|
+
|
|
173
|
+
Transients return directly without caching:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
if (token === "RequestContext") {
|
|
177
|
+
return create_RequestContext(this); // No caching!
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Dependency Injection
|
|
182
|
+
|
|
183
|
+
Dependencies are resolved recursively:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
function create_ApiService(container: NeoContainer) {
|
|
187
|
+
return new Import_0.ApiService(
|
|
188
|
+
container.resolve("ILogger"), // Resolves ILogger first
|
|
189
|
+
container.resolve("PropertyToken:...") // Resolves property
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## With Parent Container
|
|
195
|
+
|
|
196
|
+
When using `useContainer`:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Configuration
|
|
200
|
+
const child = defineBuilderConfig({
|
|
201
|
+
useContainer: parent,
|
|
202
|
+
injections: [{ token: UserService }]
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// Generated
|
|
208
|
+
import { parent } from './parent-container';
|
|
209
|
+
|
|
210
|
+
export class NeoContainer {
|
|
211
|
+
constructor(
|
|
212
|
+
private parent: typeof parent = parent, // 👈 Parent reference
|
|
213
|
+
// ...
|
|
214
|
+
) {}
|
|
215
|
+
|
|
216
|
+
resolve(token: any): any {
|
|
217
|
+
const local = this.resolveLocal(token);
|
|
218
|
+
if (local !== undefined) return local;
|
|
219
|
+
|
|
220
|
+
// Delegate to parent
|
|
221
|
+
if (this.parent) {
|
|
222
|
+
return this.parent.resolve(token);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw new Error('...');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const child = new NeoContainer(parent);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## With Factory Provider
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// Configuration
|
|
236
|
+
{
|
|
237
|
+
token: useInterface<IDatabase>(),
|
|
238
|
+
provider: (container) => {
|
|
239
|
+
const config = container.resolve(useInterface<IConfig>());
|
|
240
|
+
return new PostgresDatabase(config.connectionString);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Generated
|
|
247
|
+
function create_IDatabase(container: NeoContainer) {
|
|
248
|
+
const userFactory = (container) => {
|
|
249
|
+
const config = container.resolve("IConfig");
|
|
250
|
+
return new PostgresDatabase(config.connectionString);
|
|
251
|
+
};
|
|
252
|
+
return userFactory(container);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Bundle Size Impact
|
|
257
|
+
|
|
258
|
+
| Aspect | Traditional DI | Neo-Syringe |
|
|
259
|
+
|--------|---------------|-------------|
|
|
260
|
+
| Container library | 4-11 KB | 0 KB |
|
|
261
|
+
| reflect-metadata | ~3 KB | 0 KB |
|
|
262
|
+
| Generated code | N/A | ~50-200 lines |
|
|
263
|
+
| **Total** | **7-14 KB** | **< 1 KB** |
|
|
264
|
+
|
|
265
|
+
The generated code is:
|
|
266
|
+
- ✅ Tree-shakeable (unused services removed)
|
|
267
|
+
- ✅ Minifiable (standard JavaScript)
|
|
268
|
+
- ✅ No external dependencies
|
|
269
|
+
|
|
270
|
+
## Debugging
|
|
271
|
+
|
|
272
|
+
The generated container includes a `_graph` getter for inspection:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
console.log(container._graph);
|
|
276
|
+
// ["ILogger", "PropertyToken:ApiService.apiUrl", "ApiService"]
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Each container also has a `name` for error messages:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// Error: [AppContainer] Service not found: UnknownToken
|
|
283
|
+
```
|
|
284
|
+
|