@djodjonx/neo-syringe 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docs.yml +59 -0
- package/CHANGELOG.md +14 -0
- package/README.md +72 -779
- package/dist/cli/index.cjs +15 -0
- package/dist/cli/index.mjs +15 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/unplugin/index.cjs +31 -7
- package/dist/unplugin/index.d.cts +7 -5
- package/dist/unplugin/index.d.mts +7 -5
- package/dist/unplugin/index.mjs +31 -7
- package/docs/.vitepress/config.ts +109 -0
- package/docs/.vitepress/theme/custom.css +150 -0
- package/docs/.vitepress/theme/index.ts +17 -0
- package/docs/api/configuration.md +274 -0
- package/docs/api/functions.md +291 -0
- package/docs/api/types.md +158 -0
- package/docs/guide/basic-usage.md +267 -0
- package/docs/guide/cli.md +174 -0
- package/docs/guide/generated-code.md +284 -0
- package/docs/guide/getting-started.md +171 -0
- package/docs/guide/ide-plugin.md +203 -0
- package/docs/guide/injection-types.md +287 -0
- package/docs/guide/legacy-migration.md +333 -0
- package/docs/guide/lifecycle.md +223 -0
- package/docs/guide/parent-container.md +321 -0
- package/docs/guide/scoped-injections.md +271 -0
- package/docs/guide/what-is-neo-syringe.md +162 -0
- package/docs/guide/why-neo-syringe.md +219 -0
- package/docs/index.md +138 -0
- package/docs/public/logo.png +0 -0
- package/package.json +5 -3
- package/src/analyzer/types.ts +52 -52
- package/src/cli/index.ts +15 -0
- package/src/generator/Generator.ts +23 -1
- package/src/types.ts +1 -1
- package/src/unplugin/index.ts +13 -41
- package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
- package/tests/e2e/container-integration.test.ts +19 -19
- package/tests/e2e/generated-code.test.ts +2 -2
- package/tsconfig.json +2 -1
- package/typedoc.json +0 -5
package/src/cli/index.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Neo-Syringe CLI
|
|
4
|
+
*
|
|
5
|
+
* Validates the dependency graph for a TypeScript project.
|
|
6
|
+
* Detects circular dependencies, missing bindings, and duplicate registrations.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```bash
|
|
10
|
+
* npx neo-syringe
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
2
13
|
import * as ts from 'typescript';
|
|
3
14
|
import { Analyzer } from '../analyzer/Analyzer';
|
|
4
15
|
import { GraphValidator } from '../generator/GraphValidator';
|
|
5
16
|
|
|
17
|
+
/**
|
|
18
|
+
* CLI entry point.
|
|
19
|
+
* Reads tsconfig.json, analyzes the project, and validates the dependency graph.
|
|
20
|
+
*/
|
|
6
21
|
function main() {
|
|
7
22
|
const cwd = process.cwd();
|
|
8
23
|
const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, 'tsconfig.json');
|
|
@@ -2,11 +2,24 @@ import * as ts from 'typescript';
|
|
|
2
2
|
import { DependencyGraph, TokenId } from '../analyzer/types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Generates
|
|
5
|
+
* Generates TypeScript code for the dependency injection container.
|
|
6
|
+
*
|
|
7
|
+
* Takes a validated dependency graph and produces:
|
|
8
|
+
* - Import statements for all dependencies
|
|
9
|
+
* - Factory functions for each service
|
|
10
|
+
* - A NeoContainer class with resolve logic
|
|
6
11
|
*/
|
|
7
12
|
export class Generator {
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new Generator.
|
|
15
|
+
* @param graph - The validated dependency graph to generate code from.
|
|
16
|
+
*/
|
|
8
17
|
constructor(private graph: DependencyGraph) {}
|
|
9
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Generates the complete container code as a string.
|
|
21
|
+
* @returns TypeScript source code for the generated container.
|
|
22
|
+
*/
|
|
10
23
|
public generate(): string {
|
|
11
24
|
const sorted = this.topologicalSort();
|
|
12
25
|
const imports = new Map<string, string>(); // filePath -> importAliasPrefix
|
|
@@ -196,6 +209,10 @@ export const container = new NeoContainer(${containerArgs});
|
|
|
196
209
|
`;
|
|
197
210
|
}
|
|
198
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Sorts services in topological order (dependencies before dependents).
|
|
214
|
+
* @returns Array of TokenIds in dependency order.
|
|
215
|
+
*/
|
|
199
216
|
private topologicalSort(): TokenId[] {
|
|
200
217
|
const visited = new Set<TokenId>();
|
|
201
218
|
const sorted: TokenId[] = [];
|
|
@@ -220,6 +237,11 @@ export const container = new NeoContainer(${containerArgs});
|
|
|
220
237
|
return sorted;
|
|
221
238
|
}
|
|
222
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Creates a valid JavaScript function name from a token ID.
|
|
242
|
+
* @param tokenId - The token identifier.
|
|
243
|
+
* @returns A sanitized factory function name.
|
|
244
|
+
*/
|
|
223
245
|
private getFactoryName(tokenId: TokenId): string {
|
|
224
246
|
return `create_${tokenId.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
225
247
|
}
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Represents a generic class constructor.
|
|
3
3
|
* @template T - The type of the instance created by the constructor.
|
|
4
4
|
*/
|
|
5
|
-
export type Constructor<T =
|
|
5
|
+
export type Constructor<T = any> = new (...args: any[]) => T;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Defines the lifecycle of a service.
|
package/src/unplugin/index.ts
CHANGED
|
@@ -5,46 +5,35 @@ import { GraphValidator } from '../generator/GraphValidator';
|
|
|
5
5
|
import { Generator } from '../generator/Generator';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Neo-Syringe build plugin for Vite, Rollup, Webpack, and other bundlers.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Intercepts files containing `defineBuilderConfig` calls, analyzes the
|
|
11
|
+
* dependency graph, validates it, and replaces the configuration with
|
|
12
|
+
* generated factory code.
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
|
+
* ```typescript
|
|
15
16
|
* // vite.config.ts
|
|
16
17
|
* import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
|
|
17
18
|
*
|
|
18
19
|
* export default defineConfig({
|
|
19
|
-
* plugins: [neoSyringePlugin.vite()]
|
|
20
|
+
* plugins: [neoSyringePlugin.vite()]
|
|
20
21
|
* });
|
|
22
|
+
* ```
|
|
21
23
|
*/
|
|
22
|
-
export const neoSyringePlugin = createUnplugin((
|
|
24
|
+
export const neoSyringePlugin = createUnplugin(() => {
|
|
23
25
|
return {
|
|
24
26
|
name: 'neo-syringe-plugin',
|
|
25
|
-
|
|
26
|
-
* Includes .ts and .tsx files for transformation.
|
|
27
|
-
* @param id - The file path.
|
|
28
|
-
*/
|
|
27
|
+
|
|
29
28
|
transformInclude(id) {
|
|
30
29
|
return id.endsWith('.ts') || id.endsWith('.tsx');
|
|
31
30
|
},
|
|
32
|
-
|
|
33
|
-
* Transforms the code by replacing container definitions with generated factories.
|
|
34
|
-
* @param code - The source code.
|
|
35
|
-
* @param id - The file path.
|
|
36
|
-
*/
|
|
31
|
+
|
|
37
32
|
transform(code, id) {
|
|
38
33
|
if (!code.includes('defineBuilderConfig')) return;
|
|
39
34
|
|
|
40
|
-
// naive check, but saves performance.
|
|
41
|
-
|
|
42
|
-
// We need to parse this file to see if it really defines a container.
|
|
43
|
-
// We need a full Program to resolve types across files.
|
|
44
|
-
// Using existing tsconfig if possible.
|
|
45
|
-
|
|
46
35
|
const configFile = ts.findConfigFile(process.cwd(), ts.sys.fileExists, 'tsconfig.json');
|
|
47
|
-
if (!configFile) return;
|
|
36
|
+
if (!configFile) return;
|
|
48
37
|
|
|
49
38
|
const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
|
|
50
39
|
const { options: compilerOptions } = ts.parseJsonConfigFileContent(config, ts.sys, process.cwd());
|
|
@@ -53,30 +42,13 @@ export const neoSyringePlugin = createUnplugin((_options) => {
|
|
|
53
42
|
const analyzer = new Analyzer(program);
|
|
54
43
|
const graph = analyzer.extract();
|
|
55
44
|
|
|
56
|
-
if (graph.nodes.size === 0) return;
|
|
45
|
+
if (graph.nodes.size === 0) return;
|
|
57
46
|
|
|
58
|
-
// Validate
|
|
59
47
|
const validator = new GraphValidator();
|
|
60
48
|
validator.validate(graph);
|
|
61
49
|
|
|
62
|
-
// Generate
|
|
63
50
|
const generator = new Generator(graph);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// We need to replace the export.
|
|
67
|
-
// Strategy: Use the generated code AS the content of this file?
|
|
68
|
-
// If the file *only* exports the container, yes.
|
|
69
|
-
// But if it exports other things, we might break them.
|
|
70
|
-
|
|
71
|
-
// Safer strategy:
|
|
72
|
-
// Replace the `createContainer()...build()` expression with the object literal from generated code.
|
|
73
|
-
// But the generated code includes imports. Imports must be at top level.
|
|
74
|
-
|
|
75
|
-
// So we must REPLACE the whole file content with the generated code.
|
|
76
|
-
// This implies the user should put the container in a dedicated file.
|
|
77
|
-
// We can warn or document this.
|
|
78
|
-
|
|
79
|
-
return generatedCode;
|
|
51
|
+
return generator.generate();
|
|
80
52
|
},
|
|
81
53
|
};
|
|
82
54
|
});
|
|
@@ -80,7 +80,7 @@ describe('Analyzer - Declarative Config (defineBuilderConfig)', () => {
|
|
|
80
80
|
|
|
81
81
|
// Since we don't know the ID yet, let's iterate.
|
|
82
82
|
const nodes = Array.from(graph.nodes.values());
|
|
83
|
-
const loggerNode = nodes.find(n => n.service.implementationSymbol
|
|
83
|
+
const loggerNode = nodes.find(n => n.service.implementationSymbol?.getName() === 'ConsoleLogger');
|
|
84
84
|
|
|
85
85
|
expect(loggerNode).toBeDefined();
|
|
86
86
|
expect(loggerNode?.service.type).toBe('explicit');
|
|
@@ -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
|
|
@@ -926,7 +926,7 @@ it('should generate PropertyToken resolution', () => {
|
|
|
926
926
|
);
|
|
927
927
|
|
|
928
928
|
// Should parse without syntax errors
|
|
929
|
-
const syntaxErrors = sourceFile.parseDiagnostics || [];
|
|
929
|
+
const syntaxErrors = (sourceFile as any).parseDiagnostics || [];
|
|
930
930
|
expect(syntaxErrors.length).toBe(0);
|
|
931
931
|
});
|
|
932
932
|
|
|
@@ -948,7 +948,7 @@ it('should generate PropertyToken resolution', () => {
|
|
|
948
948
|
ts.ScriptTarget.Latest,
|
|
949
949
|
true
|
|
950
950
|
);
|
|
951
|
-
const syntaxErrors = sourceFile.parseDiagnostics || [];
|
|
951
|
+
const syntaxErrors = (sourceFile as any).parseDiagnostics || [];
|
|
952
952
|
expect(syntaxErrors.length).toBe(0);
|
|
953
953
|
});
|
|
954
954
|
});
|
package/tsconfig.json
CHANGED