@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
|
@@ -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) => {
|
|
@@ -74,7 +74,7 @@ describe('E2E - Container Integration', () => {
|
|
|
74
74
|
const container = new NeoContainer();
|
|
75
75
|
container.registerFactory(SimpleService, () => new SimpleService());
|
|
76
76
|
|
|
77
|
-
const service = container.resolve(SimpleService);
|
|
77
|
+
const service = container.resolve<any>(SimpleService);
|
|
78
78
|
expect(service.getValue()).toBe(42);
|
|
79
79
|
});
|
|
80
80
|
|
|
@@ -96,7 +96,7 @@ describe('E2E - Container Integration', () => {
|
|
|
96
96
|
container.registerFactory(Logger, () => new Logger());
|
|
97
97
|
container.registerFactory(UserService, (c) => new UserService(c.resolve(Logger)));
|
|
98
98
|
|
|
99
|
-
const userService = container.resolve(UserService);
|
|
99
|
+
const userService = container.resolve<any>(UserService);
|
|
100
100
|
expect(userService.greet('World')).toBe('Hello World');
|
|
101
101
|
});
|
|
102
102
|
});
|
|
@@ -111,8 +111,8 @@ describe('E2E - Container Integration', () => {
|
|
|
111
111
|
const container = new NeoContainer();
|
|
112
112
|
container.registerFactory(Counter, () => new Counter(), 'singleton');
|
|
113
113
|
|
|
114
|
-
const c1 = container.resolve(Counter);
|
|
115
|
-
const c2 = container.resolve(Counter);
|
|
114
|
+
const c1 = container.resolve<any>(Counter);
|
|
115
|
+
const c2 = container.resolve<any>(Counter);
|
|
116
116
|
|
|
117
117
|
expect(c1).toBe(c2);
|
|
118
118
|
expect(c1.increment()).toBe(1);
|
|
@@ -131,8 +131,8 @@ describe('E2E - Container Integration', () => {
|
|
|
131
131
|
const container = new NeoContainer();
|
|
132
132
|
container.registerFactory(Transient, () => new Transient(), 'transient');
|
|
133
133
|
|
|
134
|
-
const t1 = container.resolve(Transient);
|
|
135
|
-
const t2 = container.resolve(Transient);
|
|
134
|
+
const t1 = container.resolve<any>(Transient);
|
|
135
|
+
const t2 = container.resolve<any>(Transient);
|
|
136
136
|
|
|
137
137
|
expect(t1).not.toBe(t2);
|
|
138
138
|
expect(t1.id).not.toBe(t2.id);
|
|
@@ -163,11 +163,11 @@ describe('E2E - Container Integration', () => {
|
|
|
163
163
|
container.registerFactory(UserRepo, (c) => new UserRepo(c.resolve(Database)));
|
|
164
164
|
container.registerFactory(UserService, (c) => new UserService(c.resolve(UserRepo), c.resolve(Logger)));
|
|
165
165
|
|
|
166
|
-
const userService = container.resolve(UserService);
|
|
166
|
+
const userService = container.resolve<any>(UserService);
|
|
167
167
|
expect(userService.getDbUrl()).toBe('postgres://localhost');
|
|
168
168
|
|
|
169
169
|
// Verify singleton behavior
|
|
170
|
-
const logger1 = container.resolve(Logger);
|
|
170
|
+
const logger1 = container.resolve<any>(Logger);
|
|
171
171
|
const logger2 = userService.logger;
|
|
172
172
|
expect(logger1).toBe(logger2);
|
|
173
173
|
});
|
|
@@ -181,7 +181,7 @@ describe('E2E - Container Integration', () => {
|
|
|
181
181
|
const container = new NeoContainer(undefined, undefined, 'MyApp');
|
|
182
182
|
container.registerFactory(Registered, () => new Registered());
|
|
183
183
|
|
|
184
|
-
expect(() => container.resolve(NotRegistered)).toThrow(/MyApp.*not found/);
|
|
184
|
+
expect(() => container.resolve<any>(NotRegistered)).toThrow(/MyApp.*not found/);
|
|
185
185
|
});
|
|
186
186
|
});
|
|
187
187
|
|
|
@@ -198,7 +198,7 @@ describe('E2E - Container Integration', () => {
|
|
|
198
198
|
container.registerFactory(Right, (c) => new Right(c.resolve(Shared)));
|
|
199
199
|
container.registerFactory(Top, (c) => new Top(c.resolve(Left), c.resolve(Right)));
|
|
200
200
|
|
|
201
|
-
const top = container.resolve(Top);
|
|
201
|
+
const top = container.resolve<any>(Top);
|
|
202
202
|
|
|
203
203
|
// Both Left and Right should share the same Shared instance
|
|
204
204
|
expect(top.left.shared).toBe(top.right.shared);
|
|
@@ -217,7 +217,7 @@ describe('E2E - Container Integration', () => {
|
|
|
217
217
|
const child = new NeoContainer(parent, undefined, 'Child');
|
|
218
218
|
child.registerFactory(ChildService, (c) => new ChildService(c.resolve(SharedService)));
|
|
219
219
|
|
|
220
|
-
const service = child.resolve(ChildService);
|
|
220
|
+
const service = child.resolve<any>(ChildService);
|
|
221
221
|
expect(service.shared.value).toBe('shared');
|
|
222
222
|
});
|
|
223
223
|
|
|
@@ -230,7 +230,7 @@ describe('E2E - Container Integration', () => {
|
|
|
230
230
|
const child = new NeoContainer(parent);
|
|
231
231
|
child.registerFactory(Service, () => new Service('child'));
|
|
232
232
|
|
|
233
|
-
expect(child.resolve(Service).value).toBe('child');
|
|
233
|
+
expect(child.resolve<any>(Service).value).toBe('child');
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
|
|
@@ -248,7 +248,7 @@ describe('E2E - Container Integration', () => {
|
|
|
248
248
|
|
|
249
249
|
const container = new NeoContainer(undefined, [legacyContainer], 'App');
|
|
250
250
|
|
|
251
|
-
const service = container.resolve(LegacyService);
|
|
251
|
+
const service = container.resolve<any>(LegacyService);
|
|
252
252
|
expect(service.fromLegacy).toBe(true);
|
|
253
253
|
});
|
|
254
254
|
|
|
@@ -262,7 +262,7 @@ describe('E2E - Container Integration', () => {
|
|
|
262
262
|
const container = new NeoContainer(undefined, [legacyContainer]);
|
|
263
263
|
container.registerFactory(Service, () => new Service('local'));
|
|
264
264
|
|
|
265
|
-
expect(container.resolve(Service).source).toBe('local');
|
|
265
|
+
expect(container.resolve<any>(Service).source).toBe('local');
|
|
266
266
|
});
|
|
267
267
|
});
|
|
268
268
|
|
|
@@ -291,7 +291,7 @@ describe('E2E - Container Integration', () => {
|
|
|
291
291
|
container.registerFactory(ILOGGER, () => new ConsoleLogger());
|
|
292
292
|
container.registerFactory(UserService, (c) => new UserService(c.resolve(ILOGGER)));
|
|
293
293
|
|
|
294
|
-
const service = container.resolve(UserService);
|
|
294
|
+
const service = container.resolve<any>(UserService);
|
|
295
295
|
expect(service.logger.log('hi')).toBe('hi');
|
|
296
296
|
});
|
|
297
297
|
});
|
|
@@ -305,11 +305,11 @@ describe('E2E - Container Integration', () => {
|
|
|
305
305
|
|
|
306
306
|
const API_URL_TOKEN = 'ApiUrl';
|
|
307
307
|
container.registerFactory(API_URL_TOKEN, (c) => {
|
|
308
|
-
const config = c.resolve(Config);
|
|
308
|
+
const config = c.resolve<Config>(Config);
|
|
309
309
|
return `https://api.${config.env}.example.com`;
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
-
expect(container.resolve(API_URL_TOKEN)).toBe('https://api.test.example.com');
|
|
312
|
+
expect(container.resolve<any>(API_URL_TOKEN)).toBe('https://api.test.example.com');
|
|
313
313
|
});
|
|
314
314
|
});
|
|
315
315
|
|
|
@@ -324,7 +324,7 @@ describe('E2E - Container Integration', () => {
|
|
|
324
324
|
|
|
325
325
|
const start = Date.now();
|
|
326
326
|
for (let i = 0; i < 100; i++) {
|
|
327
|
-
container.resolve(HeavyService);
|
|
327
|
+
container.resolve<any>(HeavyService);
|
|
328
328
|
}
|
|
329
329
|
const duration = Date.now() - start;
|
|
330
330
|
|
|
@@ -348,7 +348,7 @@ describe('E2E - Container Integration', () => {
|
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
const start = Date.now();
|
|
351
|
-
container.resolve(prevClass);
|
|
351
|
+
container.resolve<any>(prevClass);
|
|
352
352
|
const duration = Date.now() - start;
|
|
353
353
|
|
|
354
354
|
expect(duration).toBeLessThan(100); // 100ms max for 20 levels
|