@djodjonx/neo-syringe 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.github/workflows/docs.yml +59 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +72 -779
  4. package/dist/cli/index.cjs +15 -0
  5. package/dist/cli/index.mjs +15 -0
  6. package/dist/index.d.cts +1 -1
  7. package/dist/index.d.mts +1 -1
  8. package/dist/unplugin/index.cjs +31 -7
  9. package/dist/unplugin/index.d.cts +7 -5
  10. package/dist/unplugin/index.d.mts +7 -5
  11. package/dist/unplugin/index.mjs +31 -7
  12. package/docs/.vitepress/config.ts +109 -0
  13. package/docs/.vitepress/theme/custom.css +150 -0
  14. package/docs/.vitepress/theme/index.ts +17 -0
  15. package/docs/api/configuration.md +274 -0
  16. package/docs/api/functions.md +291 -0
  17. package/docs/api/types.md +158 -0
  18. package/docs/guide/basic-usage.md +267 -0
  19. package/docs/guide/cli.md +174 -0
  20. package/docs/guide/generated-code.md +284 -0
  21. package/docs/guide/getting-started.md +171 -0
  22. package/docs/guide/ide-plugin.md +203 -0
  23. package/docs/guide/injection-types.md +287 -0
  24. package/docs/guide/legacy-migration.md +333 -0
  25. package/docs/guide/lifecycle.md +223 -0
  26. package/docs/guide/parent-container.md +321 -0
  27. package/docs/guide/scoped-injections.md +271 -0
  28. package/docs/guide/what-is-neo-syringe.md +162 -0
  29. package/docs/guide/why-neo-syringe.md +219 -0
  30. package/docs/index.md +138 -0
  31. package/docs/public/logo.png +0 -0
  32. package/package.json +5 -3
  33. package/src/analyzer/types.ts +52 -52
  34. package/src/cli/index.ts +15 -0
  35. package/src/generator/Generator.ts +23 -1
  36. package/src/types.ts +1 -1
  37. package/src/unplugin/index.ts +13 -41
  38. package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
  39. package/tests/e2e/container-integration.test.ts +19 -19
  40. package/tests/e2e/generated-code.test.ts +2 -2
  41. package/tsconfig.json +2 -1
  42. package/typedoc.json +0 -5
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 the TypeScript code for the dependency injection container.
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 = unknown> = new (...args: unknown[]) => T;
5
+ export type Constructor<T = any> = new (...args: any[]) => T;
6
6
 
7
7
  /**
8
8
  * Defines the lifecycle of a service.
@@ -5,46 +5,35 @@ import { GraphValidator } from '../generator/GraphValidator';
5
5
  import { Generator } from '../generator/Generator';
6
6
 
7
7
  /**
8
- * The Unplugin factory for Neo-Syringe.
8
+ * Neo-Syringe build plugin for Vite, Rollup, Webpack, and other bundlers.
9
9
  *
10
- * This plugin integrates with Vite, Rollup, Webpack, etc.
11
- * It intercepts files containing `createContainer` calls, analyzes them,
12
- * and replaces the runtime configuration code with the generated dependency graph.
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((_options) => {
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; // Cannot work without tsconfig
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; // No container found
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
- const generatedCode = generator.generate();
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.getName() === 'ConsoleLogger');
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
@@ -10,5 +10,6 @@
10
10
  "declaration": true,
11
11
  "outDir": "./dist"
12
12
  },
13
- "include": ["src"]
13
+ "include": ["src", "tests"],
14
+ "exclude": ["docs"]
14
15
  }
package/typedoc.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "entryPoints": ["src/index.ts"],
3
- "out": "docs",
4
- "hideGenerator": true
5
- }