@djodjonx/neo-syringe 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.github/workflows/docs.yml +59 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +72 -779
  4. package/dist/cli/index.cjs +15 -0
  5. package/dist/cli/index.mjs +15 -0
  6. package/dist/index.d.cts +1 -1
  7. package/dist/index.d.mts +1 -1
  8. package/dist/unplugin/index.cjs +31 -7
  9. package/dist/unplugin/index.d.cts +7 -5
  10. package/dist/unplugin/index.d.mts +7 -5
  11. package/dist/unplugin/index.mjs +31 -7
  12. package/docs/.vitepress/config.ts +109 -0
  13. package/docs/.vitepress/theme/custom.css +150 -0
  14. package/docs/.vitepress/theme/index.ts +17 -0
  15. package/docs/api/configuration.md +274 -0
  16. package/docs/api/functions.md +291 -0
  17. package/docs/api/types.md +158 -0
  18. package/docs/guide/basic-usage.md +267 -0
  19. package/docs/guide/cli.md +174 -0
  20. package/docs/guide/generated-code.md +284 -0
  21. package/docs/guide/getting-started.md +171 -0
  22. package/docs/guide/ide-plugin.md +203 -0
  23. package/docs/guide/injection-types.md +287 -0
  24. package/docs/guide/legacy-migration.md +333 -0
  25. package/docs/guide/lifecycle.md +223 -0
  26. package/docs/guide/parent-container.md +321 -0
  27. package/docs/guide/scoped-injections.md +271 -0
  28. package/docs/guide/what-is-neo-syringe.md +162 -0
  29. package/docs/guide/why-neo-syringe.md +219 -0
  30. package/docs/index.md +138 -0
  31. package/docs/public/logo.png +0 -0
  32. package/package.json +5 -3
  33. package/src/analyzer/types.ts +52 -52
  34. package/src/cli/index.ts +15 -0
  35. package/src/generator/Generator.ts +23 -1
  36. package/src/types.ts +1 -1
  37. package/src/unplugin/index.ts +13 -41
  38. package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
  39. package/tests/e2e/container-integration.test.ts +19 -19
  40. package/tests/e2e/generated-code.test.ts +2 -2
  41. package/tsconfig.json +2 -1
  42. package/typedoc.json +0 -5
@@ -0,0 +1,333 @@
1
+ # Legacy Migration
2
+
3
+ Bridge existing DI containers (tsyringe, InversifyJS) while migrating to Neo-Syringe.
4
+
5
+ ## Overview
6
+
7
+ You don't have to migrate everything at once. Neo-Syringe can delegate resolution to any container that has a `resolve()` method.
8
+
9
+ ```typescript
10
+ // Bridge your existing container
11
+ export const container = defineBuilderConfig({
12
+ useContainer: legacyContainer, // Delegate to legacy
13
+ injections: [
14
+ { token: NewService } // New services in Neo-Syringe
15
+ ]
16
+ });
17
+ ```
18
+
19
+ ## With tsyringe
20
+
21
+ ### Step 1: Keep Your Existing Setup
22
+
23
+ ```typescript
24
+ // legacy-container.ts (existing tsyringe code)
25
+ import 'reflect-metadata';
26
+ import { container, injectable } from 'tsyringe';
27
+
28
+ @injectable()
29
+ export class AuthService {
30
+ validateToken(token: string) { return true; }
31
+ }
32
+
33
+ @injectable()
34
+ export class LegacyUserRepository {
35
+ findById(id: string) { return { id, name: 'John' }; }
36
+ }
37
+
38
+ // Register in tsyringe
39
+ container.registerSingleton(AuthService);
40
+ container.registerSingleton(LegacyUserRepository);
41
+
42
+ export { container as legacyContainer };
43
+ ```
44
+
45
+ ### Step 2: Declare Legacy Tokens
46
+
47
+ Use `declareContainerTokens` for type-safety:
48
+
49
+ ```typescript
50
+ // container.ts
51
+ import { defineBuilderConfig, declareContainerTokens, useInterface } from '@djodjonx/neo-syringe';
52
+ import { legacyContainer, AuthService, LegacyUserRepository } from './legacy-container';
53
+
54
+ // Declare what the legacy container provides
55
+ const legacy = declareContainerTokens<{
56
+ AuthService: AuthService;
57
+ LegacyUserRepository: LegacyUserRepository;
58
+ }>(legacyContainer);
59
+ ```
60
+
61
+ ### Step 3: Bridge and Extend
62
+
63
+ ```typescript
64
+ // New services using Neo-Syringe
65
+ interface ILogger {
66
+ log(msg: string): void;
67
+ }
68
+
69
+ class ConsoleLogger implements ILogger {
70
+ log(msg: string) { console.log(msg); }
71
+ }
72
+
73
+ class UserService {
74
+ constructor(
75
+ private auth: AuthService, // From legacy!
76
+ private repo: LegacyUserRepository, // From legacy!
77
+ private logger: ILogger // From neo-syringe
78
+ ) {}
79
+ }
80
+
81
+ export const appContainer = defineBuilderConfig({
82
+ name: 'AppContainer',
83
+ useContainer: legacy, // 👈 Bridge to legacy
84
+ injections: [
85
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
86
+ { token: UserService }
87
+ ]
88
+ });
89
+ ```
90
+
91
+ ### Step 4: Use It
92
+
93
+ ```typescript
94
+ // main.ts
95
+ import { appContainer } from './container';
96
+
97
+ const userService = appContainer.resolve(UserService);
98
+ // ✅ AuthService and LegacyUserRepository come from tsyringe
99
+ // ✅ ILogger comes from neo-syringe
100
+ ```
101
+
102
+ ## With InversifyJS
103
+
104
+ ```typescript
105
+ // legacy-inversify.ts
106
+ import 'reflect-metadata';
107
+ import { Container, injectable } from 'inversify';
108
+
109
+ @injectable()
110
+ class DatabaseConnection {
111
+ query(sql: string) { return []; }
112
+ }
113
+
114
+ const inversifyContainer = new Container();
115
+ inversifyContainer.bind(DatabaseConnection).toSelf().inSingletonScope();
116
+
117
+ export { inversifyContainer, DatabaseConnection };
118
+ ```
119
+
120
+ ```typescript
121
+ // container.ts
122
+ import { defineBuilderConfig, declareContainerTokens } from '@djodjonx/neo-syringe';
123
+ import { inversifyContainer, DatabaseConnection } from './legacy-inversify';
124
+
125
+ const legacy = declareContainerTokens<{
126
+ DatabaseConnection: DatabaseConnection;
127
+ }>(inversifyContainer);
128
+
129
+ class ReportService {
130
+ constructor(private db: DatabaseConnection) {}
131
+ }
132
+
133
+ export const container = defineBuilderConfig({
134
+ useContainer: legacy,
135
+ injections: [
136
+ { token: ReportService }
137
+ ]
138
+ });
139
+ ```
140
+
141
+ ## With Awilix
142
+
143
+ ```typescript
144
+ // legacy-awilix.ts
145
+ import { createContainer, asClass } from 'awilix';
146
+
147
+ class EmailService {
148
+ send(to: string, subject: string) { /* ... */ }
149
+ }
150
+
151
+ const awilixContainer = createContainer();
152
+ awilixContainer.register({
153
+ emailService: asClass(EmailService).singleton()
154
+ });
155
+
156
+ // Awilix uses different API, create wrapper
157
+ export const legacyContainer = {
158
+ resolve(token: any) {
159
+ return awilixContainer.resolve(token.name ?? token);
160
+ }
161
+ };
162
+ ```
163
+
164
+ ```typescript
165
+ // container.ts
166
+ import { defineBuilderConfig, declareContainerTokens } from '@djodjonx/neo-syringe';
167
+ import { legacyContainer, EmailService } from './legacy-awilix';
168
+
169
+ const legacy = declareContainerTokens<{
170
+ EmailService: EmailService;
171
+ }>(legacyContainer);
172
+
173
+ export const container = defineBuilderConfig({
174
+ useContainer: legacy,
175
+ injections: [
176
+ { token: NotificationService } // Uses EmailService from Awilix
177
+ ]
178
+ });
179
+ ```
180
+
181
+ ## How It Works
182
+
183
+ ### At Compile-Time
184
+
185
+ 1. `declareContainerTokens<T>()` is analyzed
186
+ 2. Type `T` properties are extracted (e.g., `{ AuthService, UserRepo }`)
187
+ 3. These tokens are added to `parentProvidedTokens`
188
+ 4. GraphValidator accepts them as valid dependencies
189
+ 5. Generator outputs: `new NeoContainer(undefined, [legacyContainer])`
190
+
191
+ ### At Runtime
192
+
193
+ ```typescript
194
+ // Generated code (simplified)
195
+ class NeoContainer {
196
+ constructor(
197
+ private parent?: any,
198
+ private legacy?: any[] // ← Your tsyringe/inversify container
199
+ ) {}
200
+
201
+ resolve(token: any): any {
202
+ // 1. Try local resolution
203
+ const local = this.resolveLocal(token);
204
+ if (local !== undefined) return local;
205
+
206
+ // 2. Delegate to parent (Neo-Syringe container)
207
+ if (this.parent) {
208
+ try { return this.parent.resolve(token); }
209
+ catch { /* continue */ }
210
+ }
211
+
212
+ // 3. Delegate to legacy containers
213
+ if (this.legacy) {
214
+ for (const container of this.legacy) {
215
+ try { return container.resolve(token); } // ← Calls tsyringe!
216
+ catch { /* try next */ }
217
+ }
218
+ }
219
+
220
+ throw new Error(`Service not found: ${token}`);
221
+ }
222
+ }
223
+ ```
224
+
225
+ ## Validation
226
+
227
+ Neo-Syringe validates legacy bindings at compile-time:
228
+
229
+ | Check | Description |
230
+ |-------|-------------|
231
+ | ✅ Missing binding | Error if dependency not in local OR legacy container |
232
+ | ✅ Duplicate detection | Error if token already registered in legacy |
233
+ | ✅ Type safety | `declareContainerTokens<T>()` provides TypeScript types |
234
+
235
+ ## Migration Strategy
236
+
237
+ ### Phase 1: Bridge Everything
238
+
239
+ ```typescript
240
+ const legacy = declareContainerTokens<{
241
+ ServiceA: ServiceA;
242
+ ServiceB: ServiceB;
243
+ ServiceC: ServiceC;
244
+ // ... all services
245
+ }>(tsyringeContainer);
246
+
247
+ export const container = defineBuilderConfig({
248
+ useContainer: legacy,
249
+ injections: [] // Nothing new yet
250
+ });
251
+ ```
252
+
253
+ ### Phase 2: New Services in Neo-Syringe
254
+
255
+ ```typescript
256
+ export const container = defineBuilderConfig({
257
+ useContainer: legacy,
258
+ injections: [
259
+ { token: NewServiceD },
260
+ { token: NewServiceE }
261
+ ]
262
+ });
263
+ ```
264
+
265
+ ### Phase 3: Migrate One at a Time
266
+
267
+ ```typescript
268
+ // Remove ServiceA from legacy declaration
269
+ const legacy = declareContainerTokens<{
270
+ ServiceB: ServiceB;
271
+ ServiceC: ServiceC;
272
+ }>(tsyringeContainer);
273
+
274
+ // Add to Neo-Syringe
275
+ export const container = defineBuilderConfig({
276
+ useContainer: legacy,
277
+ injections: [
278
+ { token: ServiceA }, // Migrated!
279
+ { token: NewServiceD },
280
+ { token: NewServiceE }
281
+ ]
282
+ });
283
+ ```
284
+
285
+ ### Phase 4: Complete Migration
286
+
287
+ ```typescript
288
+ // No more legacy!
289
+ export const container = defineBuilderConfig({
290
+ injections: [
291
+ { token: ServiceA },
292
+ { token: ServiceB },
293
+ { token: ServiceC },
294
+ { token: NewServiceD },
295
+ { token: NewServiceE }
296
+ ]
297
+ });
298
+ ```
299
+
300
+ ## Tips
301
+
302
+ ### Keep Legacy Container Isolated
303
+
304
+ Put legacy code in a separate file that you can eventually delete:
305
+
306
+ ```
307
+ src/
308
+ ├── legacy/
309
+ │ └── container.ts # Will be deleted later
310
+ ├── container.ts # Neo-Syringe
311
+ └── services/
312
+ ├── legacy/ # To be migrated
313
+ └── new/ # Pure TypeScript
314
+ ```
315
+
316
+ ### Test Both Paths
317
+
318
+ Ensure services work whether resolved from legacy or Neo-Syringe:
319
+
320
+ ```typescript
321
+ describe('UserService', () => {
322
+ it('works from legacy container', () => {
323
+ const service = legacyContainer.resolve(UserService);
324
+ expect(service).toBeDefined();
325
+ });
326
+
327
+ it('works from neo-syringe container', () => {
328
+ const service = container.resolve(UserService);
329
+ expect(service).toBeDefined();
330
+ });
331
+ });
332
+ ```
333
+
@@ -0,0 +1,223 @@
1
+ # Lifecycle
2
+
3
+ Control how instances are created and managed.
4
+
5
+ ## Overview
6
+
7
+ Neo-Syringe supports two lifecycle modes:
8
+
9
+ | Lifecycle | Behavior | Default |
10
+ |-----------|----------|---------|
11
+ | `singleton` | One instance per container | ✅ Yes |
12
+ | `transient` | New instance every `resolve()` | No |
13
+
14
+ ## Singleton (Default)
15
+
16
+ By default, all services are **singletons**. The container creates one instance and reuses it.
17
+
18
+ ```typescript
19
+ export const container = defineBuilderConfig({
20
+ injections: [
21
+ { token: UserService } // Singleton by default
22
+ ]
23
+ });
24
+
25
+ const a = container.resolve(UserService);
26
+ const b = container.resolve(UserService);
27
+
28
+ console.log(a === b); // true - same instance!
29
+ ```
30
+
31
+ ### When to Use Singleton
32
+
33
+ - ✅ Stateless services (most services)
34
+ - ✅ Database connections
35
+ - ✅ Configuration objects
36
+ - ✅ Loggers
37
+ - ✅ HTTP clients
38
+
39
+ ## Transient
40
+
41
+ Use `lifecycle: 'transient'` for a new instance on every resolution.
42
+
43
+ ```typescript
44
+ export const container = defineBuilderConfig({
45
+ injections: [
46
+ { token: RequestContext, lifecycle: 'transient' }
47
+ ]
48
+ });
49
+
50
+ const a = container.resolve(RequestContext);
51
+ const b = container.resolve(RequestContext);
52
+
53
+ console.log(a === b); // false - different instances!
54
+ ```
55
+
56
+ ### When to Use Transient
57
+
58
+ - ✅ Request-scoped objects
59
+ - ✅ Stateful objects that should be isolated
60
+ - ✅ Objects with unique IDs
61
+ - ✅ Builder/Factory patterns
62
+
63
+ ## Examples
64
+
65
+ ### Request Context
66
+
67
+ ```typescript
68
+ class RequestContext {
69
+ readonly id = crypto.randomUUID();
70
+ readonly timestamp = new Date();
71
+ }
72
+
73
+ export const container = defineBuilderConfig({
74
+ injections: [
75
+ { token: RequestContext, lifecycle: 'transient' }
76
+ ]
77
+ });
78
+
79
+ // Each request gets its own context
80
+ app.use((req, res, next) => {
81
+ req.context = container.resolve(RequestContext);
82
+ next();
83
+ });
84
+ ```
85
+
86
+ ### Factory with Transient
87
+
88
+ ```typescript
89
+ {
90
+ token: useInterface<IRequest>(),
91
+ provider: () => ({
92
+ id: crypto.randomUUID(),
93
+ timestamp: Date.now()
94
+ }),
95
+ lifecycle: 'transient'
96
+ }
97
+ ```
98
+
99
+ ### Mixed Lifecycles
100
+
101
+ ```typescript
102
+ class Logger {
103
+ // Singleton - shared across all services
104
+ }
105
+
106
+ class UserService {
107
+ constructor(private logger: Logger) {}
108
+ // Singleton - one instance
109
+ }
110
+
111
+ class RequestHandler {
112
+ constructor(private userService: UserService) {}
113
+ // Transient - new instance per request
114
+ }
115
+
116
+ export const container = defineBuilderConfig({
117
+ injections: [
118
+ { token: Logger }, // singleton
119
+ { token: UserService }, // singleton
120
+ { token: RequestHandler, lifecycle: 'transient' } // transient
121
+ ]
122
+ });
123
+
124
+ // RequestHandler is new each time, but shares the same UserService
125
+ const handler1 = container.resolve(RequestHandler);
126
+ const handler2 = container.resolve(RequestHandler);
127
+
128
+ console.log(handler1 === handler2); // false
129
+ console.log(handler1.userService === handler2.userService); // true
130
+ ```
131
+
132
+ ## Generated Code
133
+
134
+ Understanding how lifecycle affects the generated code:
135
+
136
+ ### Singleton
137
+
138
+ ```typescript
139
+ // Generated for singleton
140
+ private resolveLocal(token: any): any {
141
+ if (token === UserService) {
142
+ if (!this.instances.has(UserService)) {
143
+ this.instances.set(UserService, create_UserService(this));
144
+ }
145
+ return this.instances.get(UserService);
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### Transient
151
+
152
+ ```typescript
153
+ // Generated for transient
154
+ private resolveLocal(token: any): any {
155
+ if (token === RequestContext) {
156
+ return create_RequestContext(this); // No caching!
157
+ }
158
+ }
159
+ ```
160
+
161
+ ## Scoped Lifecycle with Parent Containers
162
+
163
+ When using `scoped: true`, you can define a **different lifecycle** than the parent:
164
+
165
+ ```typescript
166
+ // Parent: ILogger is singleton
167
+ const parent = defineBuilderConfig({
168
+ injections: [
169
+ { token: useInterface<ILogger>(), provider: ConsoleLogger, lifecycle: 'singleton' }
170
+ ]
171
+ });
172
+
173
+ // Child: Override with transient lifecycle
174
+ const child = defineBuilderConfig({
175
+ useContainer: parent,
176
+ injections: [
177
+ {
178
+ token: useInterface<ILogger>(),
179
+ provider: FileLogger,
180
+ lifecycle: 'transient', // Different from parent!
181
+ scoped: true
182
+ }
183
+ ]
184
+ });
185
+
186
+ // In parent: same logger instance
187
+ const a = parent.resolve(useInterface<ILogger>());
188
+ const b = parent.resolve(useInterface<ILogger>());
189
+ console.log(a === b); // true
190
+
191
+ // In child: new instance each time
192
+ const c = child.resolve(useInterface<ILogger>());
193
+ const d = child.resolve(useInterface<ILogger>());
194
+ console.log(c === d); // false
195
+ ```
196
+
197
+ ## Best Practices
198
+
199
+ ### 1. Default to Singleton
200
+
201
+ Most services should be singletons. Only use transient when you have a specific reason.
202
+
203
+ ### 2. Transient for Stateful Objects
204
+
205
+ If an object holds request-specific state, make it transient:
206
+
207
+ ```typescript
208
+ class ShoppingCart {
209
+ items: CartItem[] = [];
210
+
211
+ addItem(item: CartItem) {
212
+ this.items.push(item);
213
+ }
214
+ }
215
+
216
+ // Each user gets their own cart
217
+ { token: ShoppingCart, lifecycle: 'transient' }
218
+ ```
219
+
220
+ ### 3. Consider Memory
221
+
222
+ Transient objects are not cached, which can increase memory churn. Profile your application if you're creating many transient instances.
223
+