@djodjonx/neo-syringe 1.1.5 → 1.2.0

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.
@@ -0,0 +1,370 @@
1
+ /**
2
+ * E2E Tests for scoped: true functionality
3
+ *
4
+ * Tests the complete flow from configuration to generated code
5
+ * for scoped token overrides.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import * as ts from 'typescript';
9
+ import { Analyzer } from '../../src/analyzer/Analyzer';
10
+ import { GraphValidator } from '../../src/generator/GraphValidator';
11
+ import { Generator } from '../../src/generator/Generator';
12
+
13
+ describe('E2E - Scoped Injections', () => {
14
+ const compileAndGenerate = (fileContent: string): string => {
15
+ const fileName = 'scoped-e2e.ts';
16
+ const compilerHost = ts.createCompilerHost({});
17
+ const originalGetSourceFile = compilerHost.getSourceFile;
18
+
19
+ compilerHost.getSourceFile = (name, languageVersion) => {
20
+ if (name === fileName) {
21
+ return ts.createSourceFile(fileName, fileContent, languageVersion);
22
+ }
23
+ return originalGetSourceFile(name, languageVersion);
24
+ };
25
+
26
+ const program = ts.createProgram([fileName], {}, compilerHost);
27
+ const analyzer = new Analyzer(program);
28
+ const graph = analyzer.extract();
29
+
30
+ const validator = new GraphValidator();
31
+ validator.validate(graph);
32
+
33
+ const generator = new Generator(graph);
34
+ return generator.generate();
35
+ };
36
+
37
+ describe('Basic scoped override', () => {
38
+ it('should compile and generate code for scoped token', () => {
39
+ const code = compileAndGenerate(`
40
+ function defineBuilderConfig(config: any) { return config; }
41
+ function useInterface<T>(): any { return null; }
42
+
43
+ interface ILogger { log(msg: string): void; }
44
+ class ConsoleLogger implements ILogger { log(msg: string) {} }
45
+ class FileLogger implements ILogger { log(msg: string) {} }
46
+
47
+ const sharedKernel = defineBuilderConfig({
48
+ name: 'SharedKernel',
49
+ injections: [
50
+ { token: useInterface<ILogger>(), provider: ConsoleLogger }
51
+ ]
52
+ });
53
+
54
+ class UserService {
55
+ constructor(private logger: ILogger) {}
56
+ }
57
+
58
+ export const userModule = defineBuilderConfig({
59
+ name: 'UserModule',
60
+ useContainer: sharedKernel,
61
+ injections: [
62
+ { token: useInterface<ILogger>(), provider: FileLogger, scoped: true },
63
+ { token: UserService }
64
+ ]
65
+ });
66
+ `);
67
+
68
+ // Container should be generated
69
+ expect(code).toContain('class NeoContainer');
70
+
71
+ // FileLogger (scoped) should be used, not ConsoleLogger
72
+ expect(code).toContain('FileLogger');
73
+
74
+ // Should have factory for ILogger (may have filename prefix)
75
+ expect(code).toMatch(/create_.*ILogger/);
76
+
77
+ // UserService should depend on ILogger
78
+ expect(code).toContain('UserService');
79
+ });
80
+
81
+ it('should fail without scoped: true when overriding parent', () => {
82
+ expect(() => compileAndGenerate(`
83
+ function defineBuilderConfig(config: any) { return config; }
84
+ function useInterface<T>(): any { return null; }
85
+
86
+ interface ILogger {}
87
+ class ConsoleLogger implements ILogger {}
88
+ class FileLogger implements ILogger {}
89
+
90
+ const parent = defineBuilderConfig({
91
+ injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
92
+ });
93
+
94
+ export const child = defineBuilderConfig({
95
+ useContainer: parent,
96
+ injections: [
97
+ { token: useInterface<ILogger>(), provider: FileLogger } // Missing scoped: true
98
+ ]
99
+ });
100
+ `)).toThrow(/Duplicate registration.*ILogger/);
101
+ });
102
+ });
103
+
104
+ describe('Scoped with scopes (singleton/transient)', () => {
105
+ it('should generate singleton for scoped singleton', () => {
106
+ const code = compileAndGenerate(`
107
+ function defineBuilderConfig(config: any) { return config; }
108
+ function useInterface<T>(): any { return null; }
109
+
110
+ interface ICache {}
111
+ class RedisCache implements ICache {}
112
+ class MemoryCache implements ICache {}
113
+
114
+ const parent = defineBuilderConfig({
115
+ injections: [{ token: useInterface<ICache>(), provider: RedisCache }]
116
+ });
117
+
118
+ export const c = defineBuilderConfig({
119
+ useContainer: parent,
120
+ injections: [
121
+ { token: useInterface<ICache>(), provider: MemoryCache, lifecycle: 'singleton', scoped: true }
122
+ ]
123
+ });
124
+ `);
125
+
126
+ // Should use instances cache for singleton
127
+ expect(code).toContain('this.instances.has');
128
+ expect(code).toContain('this.instances.set');
129
+ expect(code).toContain('MemoryCache');
130
+ });
131
+
132
+ it('should generate transient for scoped transient', () => {
133
+ const code = compileAndGenerate(`
134
+ function defineBuilderConfig(config: any) { return config; }
135
+ function useInterface<T>(): any { return null; }
136
+
137
+ interface IRequest {}
138
+ class RequestImpl implements IRequest {}
139
+
140
+ const parent = defineBuilderConfig({
141
+ injections: [{ token: useInterface<IRequest>(), provider: RequestImpl, lifecycle: 'singleton' }]
142
+ });
143
+
144
+ export const c = defineBuilderConfig({
145
+ name: 'RequestScope',
146
+ useContainer: parent,
147
+ injections: [
148
+ { token: useInterface<IRequest>(), provider: RequestImpl, lifecycle: 'transient', scoped: true }
149
+ ]
150
+ });
151
+ `);
152
+
153
+ // Transient should return directly without caching
154
+ expect(code).toMatch(/if \(token === ".*IRequest"\) \{\s*return create_/);
155
+ });
156
+ });
157
+
158
+ describe('Scoped with factory', () => {
159
+ it('should work with factory provider + scoped', () => {
160
+ const code = compileAndGenerate(`
161
+ function defineBuilderConfig(config: any) { return config; }
162
+ function useInterface<T>(): any { return null; }
163
+
164
+ interface IConfig { apiUrl: string; }
165
+
166
+ const parent = defineBuilderConfig({
167
+ injections: [{
168
+ token: useInterface<IConfig>(),
169
+ provider: () => ({ apiUrl: 'https://prod.api.com' })
170
+ }]
171
+ });
172
+
173
+ export const testModule = defineBuilderConfig({
174
+ name: 'TestModule',
175
+ useContainer: parent,
176
+ injections: [
177
+ {
178
+ token: useInterface<IConfig>(),
179
+ provider: () => ({ apiUrl: 'http://localhost:3000' }),
180
+ scoped: true
181
+ }
182
+ ]
183
+ });
184
+ `);
185
+
186
+ expect(code).toContain('localhost:3000');
187
+ expect(code).toMatch(/create_.*IConfig/);
188
+ });
189
+ });
190
+
191
+ describe('Multi-level hierarchy', () => {
192
+ it('should work with 3-level container hierarchy', () => {
193
+ const code = compileAndGenerate(`
194
+ function defineBuilderConfig(config: any) { return config; }
195
+ function useInterface<T>(): any { return null; }
196
+
197
+ interface ILogger {}
198
+ interface IDatabase {}
199
+
200
+ class ConsoleLogger implements ILogger {}
201
+ class FileLogger implements ILogger {}
202
+ class PostgresDB implements IDatabase {}
203
+
204
+ // Level 1: Infrastructure
205
+ const infrastructure = defineBuilderConfig({
206
+ name: 'Infrastructure',
207
+ injections: [
208
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
209
+ { token: useInterface<IDatabase>(), provider: PostgresDB }
210
+ ]
211
+ });
212
+
213
+ // Level 2: Domain (inherits all from infrastructure)
214
+ const domain = defineBuilderConfig({
215
+ name: 'Domain',
216
+ useContainer: infrastructure,
217
+ injections: []
218
+ });
219
+
220
+ // Level 3: TestModule (overrides ILogger only)
221
+ class TestService {
222
+ constructor(private logger: ILogger, private db: IDatabase) {}
223
+ }
224
+
225
+ export const testModule = defineBuilderConfig({
226
+ name: 'TestModule',
227
+ useContainer: domain,
228
+ injections: [
229
+ { token: useInterface<ILogger>(), provider: FileLogger, scoped: true },
230
+ { token: TestService }
231
+ ]
232
+ });
233
+ `);
234
+
235
+ // Should have local ILogger (FileLogger)
236
+ expect(code).toContain('FileLogger');
237
+
238
+ // Should NOT have local IDatabase (comes from parent)
239
+ expect(code).not.toContain('create_IDatabase');
240
+
241
+ // TestService should exist
242
+ expect(code).toContain('TestService');
243
+ });
244
+ });
245
+
246
+ describe('Class token scoped', () => {
247
+ it('should work with class token (not just interfaces)', () => {
248
+ const code = compileAndGenerate(`
249
+ function defineBuilderConfig(config: any) { return config; }
250
+
251
+ class Logger {
252
+ log(msg: string) { console.log(msg); }
253
+ }
254
+
255
+ class MockLogger extends Logger {
256
+ log(msg: string) { /* noop */ }
257
+ }
258
+
259
+ const parent = defineBuilderConfig({
260
+ injections: [{ token: Logger }]
261
+ });
262
+
263
+ export const testModule = defineBuilderConfig({
264
+ name: 'TestModule',
265
+ useContainer: parent,
266
+ injections: [
267
+ { token: Logger, provider: MockLogger, scoped: true }
268
+ ]
269
+ });
270
+ `);
271
+
272
+ expect(code).toContain('MockLogger');
273
+ expect(code).toContain('create_Logger');
274
+ });
275
+ });
276
+
277
+ describe('Multiple scoped tokens', () => {
278
+ it('should handle multiple scoped overrides', () => {
279
+ const code = compileAndGenerate(`
280
+ function defineBuilderConfig(config: any) { return config; }
281
+ function useInterface<T>(): any { return null; }
282
+
283
+ interface ILogger {}
284
+ interface ICache {}
285
+ interface IConfig {}
286
+
287
+ class ConsoleLogger implements ILogger {}
288
+ class RedisCache implements ICache {}
289
+ class ProdConfig implements IConfig {}
290
+
291
+ class MockLogger implements ILogger {}
292
+ class MemoryCache implements ICache {}
293
+ class TestConfig implements IConfig {}
294
+
295
+ const production = defineBuilderConfig({
296
+ name: 'Production',
297
+ injections: [
298
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
299
+ { token: useInterface<ICache>(), provider: RedisCache },
300
+ { token: useInterface<IConfig>(), provider: ProdConfig }
301
+ ]
302
+ });
303
+
304
+ export const testing = defineBuilderConfig({
305
+ name: 'Testing',
306
+ useContainer: production,
307
+ injections: [
308
+ { token: useInterface<ILogger>(), provider: MockLogger, scoped: true },
309
+ { token: useInterface<ICache>(), provider: MemoryCache, scoped: true },
310
+ { token: useInterface<IConfig>(), provider: TestConfig, scoped: true }
311
+ ]
312
+ });
313
+ `);
314
+
315
+ // All 3 mock implementations should be present
316
+ expect(code).toContain('MockLogger');
317
+ expect(code).toContain('MemoryCache');
318
+ expect(code).toContain('TestConfig');
319
+
320
+ // Should have factories for all 3 (may have filename prefix)
321
+ expect(code).toMatch(/create_.*ILogger/);
322
+ expect(code).toMatch(/create_.*ICache/);
323
+ expect(code).toMatch(/create_.*IConfig/);
324
+ });
325
+ });
326
+
327
+ describe('Scoped with dependencies', () => {
328
+ it('should resolve scoped dependencies correctly', () => {
329
+ const code = compileAndGenerate(`
330
+ function defineBuilderConfig(config: any) { return config; }
331
+ function useInterface<T>(): any { return null; }
332
+
333
+ interface ILogger {}
334
+ class ConsoleLogger implements ILogger {}
335
+ class FileLogger implements ILogger {}
336
+
337
+ class UserRepository {
338
+ constructor(private logger: ILogger) {}
339
+ }
340
+
341
+ class UserService {
342
+ constructor(private repo: UserRepository, private logger: ILogger) {}
343
+ }
344
+
345
+ const parent = defineBuilderConfig({
346
+ name: 'Parent',
347
+ injections: [
348
+ { token: useInterface<ILogger>(), provider: ConsoleLogger }
349
+ ]
350
+ });
351
+
352
+ export const child = defineBuilderConfig({
353
+ name: 'Child',
354
+ useContainer: parent,
355
+ injections: [
356
+ { token: useInterface<ILogger>(), provider: FileLogger, scoped: true },
357
+ { token: UserRepository },
358
+ { token: UserService }
359
+ ]
360
+ });
361
+ `);
362
+
363
+ // Both UserRepository and UserService should use local ILogger
364
+ expect(code).toContain('create_UserRepository');
365
+ expect(code).toContain('create_UserService');
366
+ expect(code).toContain('FileLogger');
367
+ });
368
+ });
369
+ });
370
+
@@ -91,8 +91,8 @@ describe('Generated Code Snapshots', () => {
91
91
  export const c = defineBuilderConfig({
92
92
  name: 'ScopesApp',
93
93
  injections: [
94
- { token: SingletonService, scope: 'singleton' },
95
- { token: TransientService, scope: 'transient' }
94
+ { token: SingletonService, lifecycle: 'singleton' },
95
+ { token: TransientService, lifecycle: 'transient' }
96
96
  ]
97
97
  });
98
98
  `);
@@ -130,8 +130,8 @@ describe('E2E - Neo-Syringe Standalone', () => {
130
130
 
131
131
  export const container = defineBuilderConfig({
132
132
  injections: [
133
- { token: SingletonService, scope: 'singleton' },
134
- { token: TransientService, scope: 'transient' }
133
+ { token: SingletonService, lifecycle: 'singleton' },
134
+ { token: TransientService, lifecycle: 'transient' }
135
135
  ]
136
136
  });
137
137
  `);
@@ -29,7 +29,7 @@ function createMockNode(
29
29
  implementationSymbol: createMockSymbol(implName, filePath),
30
30
  registrationNode: {} as ts.Node,
31
31
  type: type,
32
- scope: 'singleton',
32
+ lifecycle: 'singleton',
33
33
  } as ServiceDefinition,
34
34
  dependencies,
35
35
  };
@@ -18,7 +18,7 @@ describe('Generator - Factory Support', () => {
18
18
  tokenId: 'IConfig',
19
19
  registrationNode: {} as any,
20
20
  type: 'factory',
21
- scope: 'singleton',
21
+ lifecycle: 'singleton',
22
22
  isInterfaceToken: true,
23
23
  isFactory: true,
24
24
  factorySource: '(container) => ({ apiUrl: "http://example.com" })'
@@ -50,7 +50,7 @@ describe('Generator - Factory Support', () => {
50
50
  tokenId: 'IDatabase',
51
51
  registrationNode: {} as any,
52
52
  type: 'factory',
53
- scope: 'singleton',
53
+ lifecycle: 'singleton',
54
54
  isInterfaceToken: true,
55
55
  isFactory: true,
56
56
  factorySource: '() => ({ query: () => [] })'
@@ -78,7 +78,7 @@ describe('Generator - Factory Support', () => {
78
78
  tokenId: 'IRequest',
79
79
  registrationNode: {} as any,
80
80
  type: 'factory',
81
- scope: 'transient',
81
+ lifecycle: 'transient',
82
82
  isInterfaceToken: true,
83
83
  isFactory: true,
84
84
  factorySource: '() => ({ id: Math.random() })'
@@ -107,7 +107,7 @@ describe('Generator - Factory Support', () => {
107
107
  tokenSymbol: createMockSymbol('UserService'),
108
108
  registrationNode: {} as any,
109
109
  type: 'autowire',
110
- scope: 'singleton',
110
+ lifecycle: 'singleton',
111
111
  isFactory: false
112
112
  },
113
113
  dependencies: []
@@ -131,7 +131,7 @@ describe('Generator - Factory Support', () => {
131
131
  tokenId: 'IConfig',
132
132
  registrationNode: {} as any,
133
133
  type: 'factory',
134
- scope: 'singleton',
134
+ lifecycle: 'singleton',
135
135
  isInterfaceToken: true,
136
136
  isFactory: true,
137
137
  factorySource: '() => ({ env: "prod" })'
@@ -145,7 +145,7 @@ describe('Generator - Factory Support', () => {
145
145
  tokenSymbol: createMockSymbol('AppService'),
146
146
  registrationNode: {} as any,
147
147
  type: 'autowire',
148
- scope: 'singleton',
148
+ lifecycle: 'singleton',
149
149
  isFactory: false
150
150
  },
151
151
  dependencies: ['IConfig']
@@ -16,14 +16,14 @@ function createMockSymbol(name: string, filePath: string): ts.Symbol {
16
16
  }
17
17
 
18
18
  // Helper to create a mock node
19
- function createMockNode(id: TokenId, dependencies: TokenId[], implName: string, filePath: string, scope: 'singleton' | 'transient' = 'singleton'): DependencyNode {
19
+ function createMockNode(id: TokenId, dependencies: TokenId[], implName: string, filePath: string, lifecycle: 'singleton' | 'transient' = 'singleton'): DependencyNode {
20
20
  return {
21
21
  service: {
22
22
  tokenId: id,
23
23
  implementationSymbol: createMockSymbol(implName, filePath),
24
24
  registrationNode: {} as ts.Node,
25
25
  type: 'explicit',
26
- scope: scope,
26
+ lifecycle: lifecycle,
27
27
  } as ServiceDefinition,
28
28
  dependencies,
29
29
  };
@@ -24,7 +24,7 @@ function createNode(
24
24
  tokenId: id,
25
25
  implementationSymbol: createMockSymbol(implName, '/src/file.ts'),
26
26
  type: 'explicit',
27
- scope: 'singleton',
27
+ lifecycle: 'singleton',
28
28
  isInterfaceToken: isInterface
29
29
  } as ServiceDefinition,
30
30
  dependencies: deps
@@ -11,7 +11,7 @@ function createMockNode(id: TokenId, dependencies: TokenId[]): DependencyNode {
11
11
  implementationSymbol: {} as ts.Symbol, // Mock
12
12
  registrationNode: {} as ts.Node, // Mock
13
13
  type: 'autowire',
14
- scope: 'singleton',
14
+ lifecycle: 'singleton',
15
15
  } as ServiceDefinition,
16
16
  dependencies,
17
17
  };