@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.
- package/.github/workflows/ci.yml +6 -5
- package/CHANGELOG.md +13 -0
- package/README.md +54 -13
- 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 +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.d.cts +30 -4
- package/dist/index.d.mts +30 -4
- package/dist/lsp/index.cjs +1 -1
- package/dist/lsp/index.mjs +1 -1
- package/dist/unplugin/index.cjs +2 -2
- package/dist/unplugin/index.mjs +2 -2
- package/package.json +11 -10
- package/src/analyzer/Analyzer.ts +20 -10
- package/src/analyzer/types.ts +7 -1
- package/src/generator/Generator.ts +1 -1
- package/src/generator/GraphValidator.ts +6 -2
- package/src/types.ts +29 -3
- package/tests/analyzer/Analyzer.test.ts +4 -4
- 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 +2 -2
- package/tests/e2e/generated-code.test.ts +5 -5
- 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/src/types.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
export type Constructor<T = unknown> = new (...args: unknown[]) => T;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Defines the lifecycle
|
|
8
|
+
* Defines the lifecycle of a service.
|
|
9
9
|
* - `singleton`: One instance per container.
|
|
10
10
|
* - `transient`: A new instance every time it is resolved.
|
|
11
11
|
*/
|
|
12
|
-
export type
|
|
12
|
+
export type Lifecycle = 'singleton' | 'transient';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The dependency injection container interface.
|
|
@@ -86,7 +86,33 @@ export interface Injection<T = any> {
|
|
|
86
86
|
* Required when provider is a function, not a class.
|
|
87
87
|
*/
|
|
88
88
|
useFactory?: boolean;
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Lifecycle of the service.
|
|
91
|
+
* - `singleton`: One instance per container (default).
|
|
92
|
+
* - `transient`: A new instance every time it is resolved.
|
|
93
|
+
*/
|
|
94
|
+
lifecycle?: Lifecycle;
|
|
95
|
+
/**
|
|
96
|
+
* If true, this injection is scoped to this container only.
|
|
97
|
+
* Allows overriding a token from a parent container without causing a duplicate error.
|
|
98
|
+
* The local instance will be used instead of delegating to the parent.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const parent = defineBuilderConfig({
|
|
103
|
+
* injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* const child = defineBuilderConfig({
|
|
107
|
+
* useContainer: parent,
|
|
108
|
+
* injections: [
|
|
109
|
+
* // Override parent's ILogger with a local FileLogger
|
|
110
|
+
* { token: useInterface<ILogger>(), provider: FileLogger, scoped: true }
|
|
111
|
+
* ]
|
|
112
|
+
* });
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
scoped?: boolean;
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
/**
|
|
@@ -49,8 +49,8 @@ describe('Analyzer', () => {
|
|
|
49
49
|
class B {}
|
|
50
50
|
export const container = defineBuilderConfig({
|
|
51
51
|
injections: [
|
|
52
|
-
{ token: A,
|
|
53
|
-
{ token: B,
|
|
52
|
+
{ token: A, lifecycle: 'transient' },
|
|
53
|
+
{ token: B, lifecycle: 'singleton' }
|
|
54
54
|
]
|
|
55
55
|
});
|
|
56
56
|
`;
|
|
@@ -69,7 +69,7 @@ describe('Analyzer', () => {
|
|
|
69
69
|
const analyzer = new Analyzer(program);
|
|
70
70
|
const graph = analyzer.extract();
|
|
71
71
|
|
|
72
|
-
expect(graph.nodes.get('A')?.service.
|
|
73
|
-
expect(graph.nodes.get('B')?.service.
|
|
72
|
+
expect(graph.nodes.get('A')?.service.lifecycle).toBe('transient');
|
|
73
|
+
expect(graph.nodes.get('B')?.service.lifecycle).toBe('singleton');
|
|
74
74
|
});
|
|
75
75
|
});
|
|
@@ -151,7 +151,7 @@ describe('Analyzer - Factory Support', () => {
|
|
|
151
151
|
{
|
|
152
152
|
token: useInterface<IRequest>(),
|
|
153
153
|
provider: () => ({ id: ++counter }),
|
|
154
|
-
|
|
154
|
+
lifecycle: 'transient'
|
|
155
155
|
}
|
|
156
156
|
]
|
|
157
157
|
});
|
|
@@ -163,7 +163,7 @@ describe('Analyzer - Factory Support', () => {
|
|
|
163
163
|
|
|
164
164
|
const node = findNodeByName(graph, 'IRequest');
|
|
165
165
|
expect(node?.service.isFactory).toBe(true);
|
|
166
|
-
expect(node?.service.
|
|
166
|
+
expect(node?.service.lifecycle).toBe('transient');
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
it('should not treat class as factory', () => {
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for scoped: true functionality
|
|
3
|
+
*
|
|
4
|
+
* Allows overriding a token from a parent container
|
|
5
|
+
* without causing a duplicate registration error.
|
|
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('Scoped Injection', () => {
|
|
14
|
+
const createProgram = (fileName: string, fileContent: string) => {
|
|
15
|
+
const compilerHost = ts.createCompilerHost({});
|
|
16
|
+
const originalGetSourceFile = compilerHost.getSourceFile;
|
|
17
|
+
|
|
18
|
+
compilerHost.getSourceFile = (name, languageVersion) => {
|
|
19
|
+
if (name === fileName) {
|
|
20
|
+
return ts.createSourceFile(fileName, fileContent, languageVersion);
|
|
21
|
+
}
|
|
22
|
+
return originalGetSourceFile(name, languageVersion);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return ts.createProgram([fileName], {}, compilerHost);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('Analyzer - scoped detection', () => {
|
|
29
|
+
it('should detect scoped: true in injection', () => {
|
|
30
|
+
const program = createProgram('scoped.ts', `
|
|
31
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
32
|
+
function useInterface<T>(): any { return null; }
|
|
33
|
+
|
|
34
|
+
interface ILogger {}
|
|
35
|
+
class FileLogger implements ILogger {}
|
|
36
|
+
|
|
37
|
+
export const c = defineBuilderConfig({
|
|
38
|
+
injections: [
|
|
39
|
+
{ token: useInterface<ILogger>(), provider: FileLogger, scoped: true }
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
`);
|
|
43
|
+
|
|
44
|
+
const analyzer = new Analyzer(program);
|
|
45
|
+
const graph = analyzer.extract();
|
|
46
|
+
|
|
47
|
+
// Find node containing ILogger (may have filename prefix)
|
|
48
|
+
const node = Array.from(graph.nodes.values()).find(n =>
|
|
49
|
+
n.service.tokenId.includes('ILogger')
|
|
50
|
+
);
|
|
51
|
+
expect(node).toBeDefined();
|
|
52
|
+
expect(node!.service.isScoped).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should detect scoped: false (default) when not specified', () => {
|
|
56
|
+
const program = createProgram('not-scoped.ts', `
|
|
57
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
58
|
+
function useInterface<T>(): any { return null; }
|
|
59
|
+
|
|
60
|
+
interface ILogger {}
|
|
61
|
+
class ConsoleLogger implements ILogger {}
|
|
62
|
+
|
|
63
|
+
export const c = defineBuilderConfig({
|
|
64
|
+
injections: [
|
|
65
|
+
{ token: useInterface<ILogger>(), provider: ConsoleLogger }
|
|
66
|
+
]
|
|
67
|
+
});
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
const analyzer = new Analyzer(program);
|
|
71
|
+
const graph = analyzer.extract();
|
|
72
|
+
|
|
73
|
+
// Find node containing ILogger (may have filename prefix)
|
|
74
|
+
const node = Array.from(graph.nodes.values()).find(n =>
|
|
75
|
+
n.service.tokenId.includes('ILogger')
|
|
76
|
+
);
|
|
77
|
+
expect(node).toBeDefined();
|
|
78
|
+
expect(node!.service.isScoped).toBeFalsy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should allow duplicate registration when scoped: true in same container', () => {
|
|
82
|
+
const program = createProgram('scoped-same.ts', `
|
|
83
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
84
|
+
function useInterface<T>(): any { return null; }
|
|
85
|
+
|
|
86
|
+
interface ILogger {}
|
|
87
|
+
class FileLogger implements ILogger {}
|
|
88
|
+
|
|
89
|
+
export const c = defineBuilderConfig({
|
|
90
|
+
injections: [
|
|
91
|
+
{ token: useInterface<ILogger>(), provider: FileLogger, scoped: true },
|
|
92
|
+
{ token: useInterface<ILogger>(), provider: FileLogger, scoped: true }
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
const analyzer = new Analyzer(program);
|
|
98
|
+
// Should not throw - second scoped: true overrides the first
|
|
99
|
+
expect(() => analyzer.extract()).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('GraphValidator - scoped with parent', () => {
|
|
104
|
+
it('should throw duplicate error without scoped: true', () => {
|
|
105
|
+
const program = createProgram('duplicate.ts', `
|
|
106
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
107
|
+
function useInterface<T>(): any { return null; }
|
|
108
|
+
|
|
109
|
+
interface ILogger {}
|
|
110
|
+
class ConsoleLogger implements ILogger {}
|
|
111
|
+
class FileLogger implements ILogger {}
|
|
112
|
+
|
|
113
|
+
const parent = defineBuilderConfig({
|
|
114
|
+
injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export const c = defineBuilderConfig({
|
|
118
|
+
useContainer: parent,
|
|
119
|
+
injections: [
|
|
120
|
+
{ token: useInterface<ILogger>(), provider: FileLogger }
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
const analyzer = new Analyzer(program);
|
|
126
|
+
const graph = analyzer.extract();
|
|
127
|
+
const validator = new GraphValidator();
|
|
128
|
+
|
|
129
|
+
expect(() => validator.validate(graph)).toThrow(/Duplicate registration.*ILogger.*parent/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should NOT throw duplicate error with scoped: true', () => {
|
|
133
|
+
const program = createProgram('scoped-override.ts', `
|
|
134
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
135
|
+
function useInterface<T>(): any { return null; }
|
|
136
|
+
|
|
137
|
+
interface ILogger {}
|
|
138
|
+
class ConsoleLogger implements ILogger {}
|
|
139
|
+
class FileLogger implements ILogger {}
|
|
140
|
+
|
|
141
|
+
const parent = defineBuilderConfig({
|
|
142
|
+
injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const c = defineBuilderConfig({
|
|
146
|
+
useContainer: parent,
|
|
147
|
+
injections: [
|
|
148
|
+
{ token: useInterface<ILogger>(), provider: FileLogger, scoped: true }
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
const analyzer = new Analyzer(program);
|
|
154
|
+
const graph = analyzer.extract();
|
|
155
|
+
const validator = new GraphValidator();
|
|
156
|
+
|
|
157
|
+
expect(() => validator.validate(graph)).not.toThrow();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should suggest scoped: true in error message', () => {
|
|
161
|
+
const program = createProgram('suggest-scoped.ts', `
|
|
162
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
163
|
+
function useInterface<T>(): any { return null; }
|
|
164
|
+
|
|
165
|
+
interface ICache {}
|
|
166
|
+
class RedisCache implements ICache {}
|
|
167
|
+
class MemoryCache implements ICache {}
|
|
168
|
+
|
|
169
|
+
const parent = defineBuilderConfig({
|
|
170
|
+
injections: [{ token: useInterface<ICache>(), provider: RedisCache }]
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export const c = defineBuilderConfig({
|
|
174
|
+
useContainer: parent,
|
|
175
|
+
injections: [
|
|
176
|
+
{ token: useInterface<ICache>(), provider: MemoryCache }
|
|
177
|
+
]
|
|
178
|
+
});
|
|
179
|
+
`);
|
|
180
|
+
|
|
181
|
+
const analyzer = new Analyzer(program);
|
|
182
|
+
const graph = analyzer.extract();
|
|
183
|
+
const validator = new GraphValidator();
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
validator.validate(graph);
|
|
187
|
+
expect.fail('Should have thrown');
|
|
188
|
+
} catch (e: any) {
|
|
189
|
+
expect(e.message).toContain('scoped: true');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Generator - scoped code generation', () => {
|
|
195
|
+
it('should generate local factory for scoped token', () => {
|
|
196
|
+
const program = createProgram('gen-scoped.ts', `
|
|
197
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
198
|
+
function useInterface<T>(): any { return null; }
|
|
199
|
+
|
|
200
|
+
interface ILogger {}
|
|
201
|
+
class ConsoleLogger implements ILogger {}
|
|
202
|
+
class FileLogger implements ILogger {}
|
|
203
|
+
|
|
204
|
+
const parent = defineBuilderConfig({
|
|
205
|
+
injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export const c = defineBuilderConfig({
|
|
209
|
+
name: 'ChildContainer',
|
|
210
|
+
useContainer: parent,
|
|
211
|
+
injections: [
|
|
212
|
+
{ token: useInterface<ILogger>(), provider: FileLogger, scoped: true }
|
|
213
|
+
]
|
|
214
|
+
});
|
|
215
|
+
`);
|
|
216
|
+
|
|
217
|
+
const analyzer = new Analyzer(program);
|
|
218
|
+
const graph = analyzer.extract();
|
|
219
|
+
|
|
220
|
+
// Skip validation for this test (it would throw duplicate without scoped handling in validator)
|
|
221
|
+
// The generator should include the scoped token factory
|
|
222
|
+
const generator = new Generator(graph);
|
|
223
|
+
const code = generator.generate();
|
|
224
|
+
|
|
225
|
+
// Should have a factory for ILogger (locally) - may have filename prefix
|
|
226
|
+
expect(code).toMatch(/create_.*ILogger/);
|
|
227
|
+
expect(code).toContain('FileLogger');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should resolve scoped token locally before parent', () => {
|
|
231
|
+
const program = createProgram('resolve-local.ts', `
|
|
232
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
233
|
+
function useInterface<T>(): any { return null; }
|
|
234
|
+
|
|
235
|
+
interface ILogger {}
|
|
236
|
+
class ConsoleLogger implements ILogger {}
|
|
237
|
+
class FileLogger implements ILogger {}
|
|
238
|
+
|
|
239
|
+
const parent = defineBuilderConfig({
|
|
240
|
+
injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
class UserService {
|
|
244
|
+
constructor(logger: ILogger) {}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const c = defineBuilderConfig({
|
|
248
|
+
name: 'ChildContainer',
|
|
249
|
+
useContainer: parent,
|
|
250
|
+
injections: [
|
|
251
|
+
{ token: useInterface<ILogger>(), provider: FileLogger, scoped: true },
|
|
252
|
+
{ token: UserService }
|
|
253
|
+
]
|
|
254
|
+
});
|
|
255
|
+
`);
|
|
256
|
+
|
|
257
|
+
const analyzer = new Analyzer(program);
|
|
258
|
+
const graph = analyzer.extract();
|
|
259
|
+
const generator = new Generator(graph);
|
|
260
|
+
const code = generator.generate();
|
|
261
|
+
|
|
262
|
+
// UserService should resolve ILogger from local container
|
|
263
|
+
expect(code).toContain('create_UserService');
|
|
264
|
+
expect(code).toContain('container.resolve');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('Scoped with different scopes (singleton/transient)', () => {
|
|
269
|
+
it('should allow scoped + transient', () => {
|
|
270
|
+
const program = createProgram('scoped-transient.ts', `
|
|
271
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
272
|
+
function useInterface<T>(): any { return null; }
|
|
273
|
+
|
|
274
|
+
interface ILogger {}
|
|
275
|
+
class ConsoleLogger implements ILogger {}
|
|
276
|
+
class FileLogger implements ILogger {}
|
|
277
|
+
|
|
278
|
+
const parent = defineBuilderConfig({
|
|
279
|
+
injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger, lifecycle: 'singleton' }]
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
export const c = defineBuilderConfig({
|
|
283
|
+
useContainer: parent,
|
|
284
|
+
injections: [
|
|
285
|
+
{
|
|
286
|
+
token: useInterface<ILogger>(),
|
|
287
|
+
provider: FileLogger,
|
|
288
|
+
lifecycle: 'transient', // Different scope than parent!
|
|
289
|
+
scoped: true
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
});
|
|
293
|
+
`);
|
|
294
|
+
|
|
295
|
+
const analyzer = new Analyzer(program);
|
|
296
|
+
const graph = analyzer.extract();
|
|
297
|
+
|
|
298
|
+
// Find node containing ILogger (may have filename prefix)
|
|
299
|
+
const node = Array.from(graph.nodes.values()).find(n =>
|
|
300
|
+
n.service.tokenId.includes('ILogger')
|
|
301
|
+
);
|
|
302
|
+
expect(node).toBeDefined();
|
|
303
|
+
expect(node!.service.lifecycle).toBe('transient');
|
|
304
|
+
expect(node!.service.isScoped).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should generate transient code for scoped transient token', () => {
|
|
308
|
+
const program = createProgram('gen-scoped-transient.ts', `
|
|
309
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
310
|
+
function useInterface<T>(): any { return null; }
|
|
311
|
+
|
|
312
|
+
interface IRequest {}
|
|
313
|
+
class RequestImpl implements IRequest {}
|
|
314
|
+
|
|
315
|
+
const parent = defineBuilderConfig({
|
|
316
|
+
injections: [{ token: useInterface<IRequest>(), provider: RequestImpl, lifecycle: 'singleton' }]
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
export const c = defineBuilderConfig({
|
|
320
|
+
name: 'RequestScope',
|
|
321
|
+
useContainer: parent,
|
|
322
|
+
injections: [
|
|
323
|
+
{
|
|
324
|
+
token: useInterface<IRequest>(),
|
|
325
|
+
provider: RequestImpl,
|
|
326
|
+
lifecycle: 'transient',
|
|
327
|
+
scoped: true
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
});
|
|
331
|
+
`);
|
|
332
|
+
|
|
333
|
+
const analyzer = new Analyzer(program);
|
|
334
|
+
const graph = analyzer.extract();
|
|
335
|
+
const generator = new Generator(graph);
|
|
336
|
+
const code = generator.generate();
|
|
337
|
+
|
|
338
|
+
// Transient should NOT use instances cache
|
|
339
|
+
expect(code).toMatch(/create_.*IRequest/);
|
|
340
|
+
// The transient resolution should return directly without caching
|
|
341
|
+
expect(code).toMatch(/if \(token === ".*IRequest"\) \{\s*return create_/);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('Complex scenarios', () => {
|
|
346
|
+
it('should work with multi-level hierarchy', () => {
|
|
347
|
+
const program = createProgram('multi-level.ts', `
|
|
348
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
349
|
+
function useInterface<T>(): any { return null; }
|
|
350
|
+
|
|
351
|
+
interface ILogger {}
|
|
352
|
+
class ConsoleLogger implements ILogger {}
|
|
353
|
+
class FileLogger implements ILogger {}
|
|
354
|
+
class MockLogger implements ILogger {}
|
|
355
|
+
|
|
356
|
+
// Level 1: Infrastructure
|
|
357
|
+
const infrastructure = defineBuilderConfig({
|
|
358
|
+
name: 'Infrastructure',
|
|
359
|
+
injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Level 2: Domain (uses parent's logger)
|
|
363
|
+
const domain = defineBuilderConfig({
|
|
364
|
+
name: 'Domain',
|
|
365
|
+
useContainer: infrastructure,
|
|
366
|
+
injections: []
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Level 3: Test (overrides logger)
|
|
370
|
+
export const testModule = defineBuilderConfig({
|
|
371
|
+
name: 'TestModule',
|
|
372
|
+
useContainer: domain,
|
|
373
|
+
injections: [
|
|
374
|
+
{ token: useInterface<ILogger>(), provider: MockLogger, scoped: true }
|
|
375
|
+
]
|
|
376
|
+
});
|
|
377
|
+
`);
|
|
378
|
+
|
|
379
|
+
const analyzer = new Analyzer(program);
|
|
380
|
+
const graph = analyzer.extract();
|
|
381
|
+
const validator = new GraphValidator();
|
|
382
|
+
|
|
383
|
+
expect(() => validator.validate(graph)).not.toThrow();
|
|
384
|
+
|
|
385
|
+
// Find the ILogger node (may have filename prefix)
|
|
386
|
+
const loggerNode = Array.from(graph.nodes.values()).find(n =>
|
|
387
|
+
n.service.tokenId.includes('ILogger')
|
|
388
|
+
);
|
|
389
|
+
expect(loggerNode?.service.isScoped).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should work with factory + scoped', () => {
|
|
393
|
+
const program = createProgram('factory-scoped.ts', `
|
|
394
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
395
|
+
function useInterface<T>(): any { return null; }
|
|
396
|
+
|
|
397
|
+
interface IConfig { env: string; }
|
|
398
|
+
|
|
399
|
+
const parent = defineBuilderConfig({
|
|
400
|
+
injections: [{
|
|
401
|
+
token: useInterface<IConfig>(),
|
|
402
|
+
provider: () => ({ env: 'production' })
|
|
403
|
+
}]
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
export const c = defineBuilderConfig({
|
|
407
|
+
useContainer: parent,
|
|
408
|
+
injections: [
|
|
409
|
+
{
|
|
410
|
+
token: useInterface<IConfig>(),
|
|
411
|
+
provider: () => ({ env: 'test' }),
|
|
412
|
+
scoped: true
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
`);
|
|
417
|
+
|
|
418
|
+
const analyzer = new Analyzer(program);
|
|
419
|
+
const graph = analyzer.extract();
|
|
420
|
+
const validator = new GraphValidator();
|
|
421
|
+
|
|
422
|
+
expect(() => validator.validate(graph)).not.toThrow();
|
|
423
|
+
|
|
424
|
+
// Find the node by checking all nodes for IConfig
|
|
425
|
+
const configNode = Array.from(graph.nodes.values()).find(n =>
|
|
426
|
+
n.service.tokenId.includes('IConfig')
|
|
427
|
+
);
|
|
428
|
+
expect(configNode).toBeDefined();
|
|
429
|
+
expect(configNode!.service.isScoped).toBe(true);
|
|
430
|
+
expect(configNode!.service.isFactory).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
package/tests/cli/cli.test.ts
CHANGED
|
@@ -225,5 +225,96 @@ describe('CLI - Validation Logic', () => {
|
|
|
225
225
|
expect(() => validator.validate(graph)).toThrow(/Circular/);
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
|
+
|
|
229
|
+
describe('Scoped Injections', () => {
|
|
230
|
+
it('should pass validation with scoped: true override', () => {
|
|
231
|
+
const program = createProgram('scoped-valid.ts', `
|
|
232
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
233
|
+
function useInterface<T>(): any { return null; }
|
|
234
|
+
|
|
235
|
+
interface ILogger {}
|
|
236
|
+
class ConsoleLogger implements ILogger {}
|
|
237
|
+
class FileLogger implements ILogger {}
|
|
238
|
+
|
|
239
|
+
const parent = defineBuilderConfig({
|
|
240
|
+
injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
export const c = defineBuilderConfig({
|
|
244
|
+
useContainer: parent,
|
|
245
|
+
injections: [
|
|
246
|
+
{ token: useInterface<ILogger>(), provider: FileLogger, scoped: true }
|
|
247
|
+
]
|
|
248
|
+
});
|
|
249
|
+
`);
|
|
250
|
+
|
|
251
|
+
const analyzer = new Analyzer(program);
|
|
252
|
+
const graph = analyzer.extract();
|
|
253
|
+
const validator = new GraphValidator();
|
|
254
|
+
|
|
255
|
+
expect(() => validator.validate(graph)).not.toThrow();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should fail validation without scoped: true', () => {
|
|
259
|
+
const program = createProgram('scoped-invalid.ts', `
|
|
260
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
261
|
+
function useInterface<T>(): any { return null; }
|
|
262
|
+
|
|
263
|
+
interface ILogger {}
|
|
264
|
+
class ConsoleLogger implements ILogger {}
|
|
265
|
+
class FileLogger implements ILogger {}
|
|
266
|
+
|
|
267
|
+
const parent = defineBuilderConfig({
|
|
268
|
+
injections: [{ token: useInterface<ILogger>(), provider: ConsoleLogger }]
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
export const c = defineBuilderConfig({
|
|
272
|
+
useContainer: parent,
|
|
273
|
+
injections: [
|
|
274
|
+
{ token: useInterface<ILogger>(), provider: FileLogger }
|
|
275
|
+
]
|
|
276
|
+
});
|
|
277
|
+
`);
|
|
278
|
+
|
|
279
|
+
const analyzer = new Analyzer(program);
|
|
280
|
+
const graph = analyzer.extract();
|
|
281
|
+
const validator = new GraphValidator();
|
|
282
|
+
|
|
283
|
+
expect(() => validator.validate(graph)).toThrow(/Duplicate registration.*ILogger/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should suggest scoped: true in error message', () => {
|
|
287
|
+
const program = createProgram('scoped-suggest.ts', `
|
|
288
|
+
function defineBuilderConfig(config: any) { return config; }
|
|
289
|
+
function useInterface<T>(): any { return null; }
|
|
290
|
+
|
|
291
|
+
interface ICache {}
|
|
292
|
+
class RedisCache implements ICache {}
|
|
293
|
+
class MemoryCache implements ICache {}
|
|
294
|
+
|
|
295
|
+
const parent = defineBuilderConfig({
|
|
296
|
+
injections: [{ token: useInterface<ICache>(), provider: RedisCache }]
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
export const c = defineBuilderConfig({
|
|
300
|
+
useContainer: parent,
|
|
301
|
+
injections: [
|
|
302
|
+
{ token: useInterface<ICache>(), provider: MemoryCache }
|
|
303
|
+
]
|
|
304
|
+
});
|
|
305
|
+
`);
|
|
306
|
+
|
|
307
|
+
const analyzer = new Analyzer(program);
|
|
308
|
+
const graph = analyzer.extract();
|
|
309
|
+
const validator = new GraphValidator();
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
validator.validate(graph);
|
|
313
|
+
expect.fail('Should have thrown');
|
|
314
|
+
} catch (e: any) {
|
|
315
|
+
expect(e.message).toContain("scoped: true");
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
228
319
|
});
|
|
229
320
|
|
|
@@ -22,8 +22,8 @@ describe('E2E - Container Integration', () => {
|
|
|
22
22
|
|
|
23
23
|
private factories = new Map<any, (container: NeoContainer) => any>();
|
|
24
24
|
|
|
25
|
-
registerFactory(token: any, factory: (container: NeoContainer) => any,
|
|
26
|
-
if (
|
|
25
|
+
registerFactory(token: any, factory: (container: NeoContainer) => any, lifecycle: 'singleton' | 'transient' = 'singleton') {
|
|
26
|
+
if (lifecycle === 'transient') {
|
|
27
27
|
this.factories.set(token, (c) => factory(c));
|
|
28
28
|
} else {
|
|
29
29
|
this.factories.set(token, (c) => {
|
|
@@ -202,7 +202,7 @@ it('should resolve class dependencies on interfaces', () => {
|
|
|
202
202
|
function defineBuilderConfig(config: any) { return config; }
|
|
203
203
|
class SingletonService {}
|
|
204
204
|
export const c = defineBuilderConfig({
|
|
205
|
-
injections: [{ token: SingletonService,
|
|
205
|
+
injections: [{ token: SingletonService, lifecycle: 'singleton' }]
|
|
206
206
|
});
|
|
207
207
|
`);
|
|
208
208
|
|
|
@@ -217,7 +217,7 @@ it('should resolve class dependencies on interfaces', () => {
|
|
|
217
217
|
function defineBuilderConfig(config: any) { return config; }
|
|
218
218
|
class TransientService {}
|
|
219
219
|
export const c = defineBuilderConfig({
|
|
220
|
-
injections: [{ token: TransientService,
|
|
220
|
+
injections: [{ token: TransientService, lifecycle: 'transient' }]
|
|
221
221
|
});
|
|
222
222
|
`);
|
|
223
223
|
|
|
@@ -652,10 +652,10 @@ it('should generate PropertyToken resolution', () => {
|
|
|
652
652
|
|
|
653
653
|
export const c = defineBuilderConfig({
|
|
654
654
|
injections: [
|
|
655
|
-
{ token: SingletonA,
|
|
655
|
+
{ token: SingletonA, lifecycle: 'singleton' },
|
|
656
656
|
{ token: SingletonB }, // Default singleton
|
|
657
|
-
{ token: TransientA,
|
|
658
|
-
{ token: TransientB,
|
|
657
|
+
{ token: TransientA, lifecycle: 'transient' },
|
|
658
|
+
{ token: TransientB, lifecycle: 'transient' }
|
|
659
659
|
]
|
|
660
660
|
});
|
|
661
661
|
`);
|