@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.
Files changed (61) hide show
  1. package/.github/workflows/ci.yml +6 -5
  2. package/.github/workflows/docs.yml +59 -0
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +74 -740
  5. package/dist/{GraphValidator-G0F4QiLk.cjs → GraphValidator-CV4VoJl0.cjs} +18 -10
  6. package/dist/{GraphValidator-C8ldJtNp.mjs → GraphValidator-DXqqkNdS.mjs} +18 -10
  7. package/dist/cli/index.cjs +16 -1
  8. package/dist/cli/index.mjs +16 -1
  9. package/dist/index.d.cts +31 -5
  10. package/dist/index.d.mts +31 -5
  11. package/dist/lsp/index.cjs +1 -1
  12. package/dist/lsp/index.mjs +1 -1
  13. package/dist/unplugin/index.cjs +33 -9
  14. package/dist/unplugin/index.d.cts +7 -5
  15. package/dist/unplugin/index.d.mts +7 -5
  16. package/dist/unplugin/index.mjs +33 -9
  17. package/docs/.vitepress/config.ts +109 -0
  18. package/docs/.vitepress/theme/custom.css +150 -0
  19. package/docs/.vitepress/theme/index.ts +17 -0
  20. package/docs/api/configuration.md +274 -0
  21. package/docs/api/functions.md +291 -0
  22. package/docs/api/types.md +158 -0
  23. package/docs/guide/basic-usage.md +267 -0
  24. package/docs/guide/cli.md +174 -0
  25. package/docs/guide/generated-code.md +284 -0
  26. package/docs/guide/getting-started.md +171 -0
  27. package/docs/guide/ide-plugin.md +203 -0
  28. package/docs/guide/injection-types.md +287 -0
  29. package/docs/guide/legacy-migration.md +333 -0
  30. package/docs/guide/lifecycle.md +223 -0
  31. package/docs/guide/parent-container.md +321 -0
  32. package/docs/guide/scoped-injections.md +271 -0
  33. package/docs/guide/what-is-neo-syringe.md +162 -0
  34. package/docs/guide/why-neo-syringe.md +219 -0
  35. package/docs/index.md +138 -0
  36. package/docs/public/logo.png +0 -0
  37. package/package.json +15 -12
  38. package/src/analyzer/Analyzer.ts +20 -10
  39. package/src/analyzer/types.ts +55 -49
  40. package/src/cli/index.ts +15 -0
  41. package/src/generator/Generator.ts +24 -2
  42. package/src/generator/GraphValidator.ts +6 -2
  43. package/src/types.ts +30 -4
  44. package/src/unplugin/index.ts +13 -41
  45. package/tests/analyzer/Analyzer.test.ts +4 -4
  46. package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
  47. package/tests/analyzer/Factory.test.ts +2 -2
  48. package/tests/analyzer/Scoped.test.ts +434 -0
  49. package/tests/cli/cli.test.ts +91 -0
  50. package/tests/e2e/container-integration.test.ts +21 -21
  51. package/tests/e2e/generated-code.test.ts +7 -7
  52. package/tests/e2e/scoped.test.ts +370 -0
  53. package/tests/e2e/snapshots.test.ts +2 -2
  54. package/tests/e2e/standalone.test.ts +2 -2
  55. package/tests/generator/ExternalGenerator.test.ts +1 -1
  56. package/tests/generator/FactoryGenerator.test.ts +6 -6
  57. package/tests/generator/Generator.test.ts +2 -2
  58. package/tests/generator/GeneratorDeclarative.test.ts +1 -1
  59. package/tests/generator/GraphValidator.test.ts +1 -1
  60. package/tsconfig.json +2 -1
  61. package/typedoc.json +0 -5
@@ -0,0 +1,203 @@
1
+ # IDE Plugin
2
+
3
+ Get real-time error detection in your editor.
4
+
5
+ ## Overview
6
+
7
+ The Neo-Syringe LSP plugin integrates with TypeScript's language service to provide:
8
+
9
+ - 🔴 **Circular dependency detection** - Instant feedback when you create cycles
10
+ - 🔴 **Missing binding detection** - Errors when dependencies aren't registered
11
+ - 🔴 **Duplicate registration detection** - Warnings for conflicts
12
+ - 💡 **Suggestions** - Helpful tips to fix issues
13
+
14
+ ## Setup
15
+
16
+ ### Step 1: Add to `tsconfig.json`
17
+
18
+ ```json
19
+ {
20
+ "compilerOptions": {
21
+ "plugins": [
22
+ { "name": "@djodjonx/neo-syringe/lsp" }
23
+ ]
24
+ }
25
+ }
26
+ ```
27
+
28
+ ### Step 2: Use Workspace TypeScript
29
+
30
+ The plugin only works with the workspace TypeScript version.
31
+
32
+ #### VS Code
33
+
34
+ 1. Open Command Palette: `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac)
35
+ 2. Search for: **"TypeScript: Select TypeScript Version"**
36
+ 3. Select: **"Use Workspace Version"**
37
+
38
+ #### JetBrains IDEs (WebStorm, IntelliJ)
39
+
40
+ 1. Go to **Settings** → **Languages & Frameworks** → **TypeScript**
41
+ 2. Set TypeScript version to **"Project"** or point to `node_modules/typescript`
42
+
43
+ #### Neovim (with nvim-lspconfig)
44
+
45
+ ```lua
46
+ require('lspconfig').tsserver.setup {
47
+ init_options = {
48
+ plugins = {
49
+ {
50
+ name = "@djodjonx/neo-syringe/lsp",
51
+ location = "./node_modules/@djodjonx/neo-syringe/dist/lsp"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### Step 3: Restart TypeScript Server
59
+
60
+ After configuration, restart the TypeScript server:
61
+
62
+ - **VS Code**: `Ctrl+Shift+P` → "TypeScript: Restart TS Server"
63
+ - **WebStorm**: File → Invalidate Caches / Restart
64
+
65
+ ## Detected Errors
66
+
67
+ ### Circular Dependency
68
+
69
+ ```typescript
70
+ class A {
71
+ constructor(private b: B) {}
72
+ }
73
+
74
+ class B {
75
+ constructor(private a: A) {} // 🔴 Cycle!
76
+ }
77
+
78
+ export const container = defineBuilderConfig({
79
+ injections: [
80
+ { token: A },
81
+ { token: B }
82
+ ]
83
+ });
84
+ // Error: [Neo-Syringe] Circular dependency detected: A -> B -> A
85
+ ```
86
+
87
+ ### Missing Binding
88
+
89
+ ```typescript
90
+ interface ILogger {
91
+ log(msg: string): void;
92
+ }
93
+
94
+ class UserService {
95
+ constructor(private logger: ILogger) {}
96
+ }
97
+
98
+ export const container = defineBuilderConfig({
99
+ injections: [
100
+ { token: UserService } // 🔴 ILogger not registered!
101
+ ]
102
+ });
103
+ // Error: [Neo-Syringe] Missing binding: 'UserService' depends on 'ILogger',
104
+ // but no provider registered.
105
+ ```
106
+
107
+ ### Duplicate Registration
108
+
109
+ ```typescript
110
+ const parent = defineBuilderConfig({
111
+ injections: [
112
+ { token: useInterface<ILogger>(), provider: ConsoleLogger }
113
+ ]
114
+ });
115
+
116
+ export const child = defineBuilderConfig({
117
+ useContainer: parent,
118
+ injections: [
119
+ { token: useInterface<ILogger>(), provider: FileLogger } // 🔴 Duplicate!
120
+ ]
121
+ });
122
+ // Error: [Neo-Syringe] Duplicate registration: 'ILogger' is already registered
123
+ // in the parent container. Use 'scoped: true' to override intentionally.
124
+ ```
125
+
126
+ ## Screenshots
127
+
128
+ ### Error in Editor
129
+
130
+ ```
131
+ ┌─────────────────────────────────────────────────────────────────┐
132
+ │ container.ts │
133
+ ├─────────────────────────────────────────────────────────────────┤
134
+ │ │
135
+ │ export const container = defineBuilderConfig({ │
136
+ │ injections: [ │
137
+ │ { token: UserService } │
138
+ │ ~~~~~~~~~~~ │
139
+ │ ▲ │
140
+ │ │ │
141
+ │ ┌────────────┴───────────────────────────────────────────────┐ │
142
+ │ │ 🔴 [Neo-Syringe] Missing binding: 'UserService' depends │ │
143
+ │ │ on 'ILogger', but no provider registered. │ │
144
+ │ └────────────────────────────────────────────────────────────┘ │
145
+ │ │
146
+ └─────────────────────────────────────────────────────────────────┘
147
+ ```
148
+
149
+ ### Quick Fix Available
150
+
151
+ ```
152
+ ┌────────────────────────────────────────────┐
153
+ │ Quick Fix │
154
+ ├────────────────────────────────────────────┤
155
+ │ 💡 Add missing injection for ILogger │
156
+ │ 💡 Mark as optional dependency │
157
+ │ 💡 Ignore this error │
158
+ └────────────────────────────────────────────┘
159
+ ```
160
+
161
+ ## Troubleshooting
162
+
163
+ ### Plugin Not Working
164
+
165
+ 1. **Check TypeScript version**: Must use workspace version
166
+ 2. **Check tsconfig.json**: Plugin must be in `compilerOptions.plugins`
167
+ 3. **Restart TS Server**: Changes require restart
168
+ 4. **Check node_modules**: Package must be installed
169
+
170
+ ### Errors Not Showing
171
+
172
+ 1. **Save the file**: Some editors need file save to trigger
173
+ 2. **Check file extension**: Only `.ts` and `.tsx` files
174
+ 3. **Check if file has `defineBuilderConfig`**: Plugin only analyzes DI files
175
+
176
+ ### False Positives
177
+
178
+ If the plugin reports errors that shouldn't exist:
179
+
180
+ 1. Check that all imports are correct
181
+ 2. Ensure interface names match
182
+ 3. Try restarting the TypeScript server
183
+
184
+ ## Configuration
185
+
186
+ Currently, the plugin uses default settings. Future versions may support:
187
+
188
+ ```json
189
+ {
190
+ "compilerOptions": {
191
+ "plugins": [
192
+ {
193
+ "name": "@djodjonx/neo-syringe/lsp",
194
+ "options": {
195
+ "strictMode": true,
196
+ "warnOnUnusedProviders": true
197
+ }
198
+ }
199
+ ]
200
+ }
201
+ }
202
+ ```
203
+
@@ -0,0 +1,287 @@
1
+ # Injection Types
2
+
3
+ All the ways to define and inject dependencies in Neo-Syringe.
4
+
5
+ ## Overview
6
+
7
+ | Type | Syntax | Use Case |
8
+ |------|--------|----------|
9
+ | **Class** | `{ token: MyClass }` | Simple autowiring |
10
+ | **Interface** | `{ token: useInterface<IFoo>(), provider: Foo }` | Abstraction |
11
+ | **Explicit** | `{ token: MyClass, provider: OtherClass }` | Override default |
12
+ | **Factory** | `{ token: X, provider: (c) => ... }` | Dynamic creation |
13
+ | **Property** | `{ token: useProperty(Class, 'param') }` | Primitives |
14
+
15
+ ## Class Token
16
+
17
+ Register a class directly. Dependencies are resolved automatically from constructor parameters.
18
+
19
+ ```typescript
20
+ class Repository {
21
+ findAll() { return []; }
22
+ }
23
+
24
+ class Service {
25
+ constructor(private repo: Repository) {}
26
+ }
27
+
28
+ export const container = defineBuilderConfig({
29
+ injections: [
30
+ { token: Repository },
31
+ { token: Service } // Repository injected automatically
32
+ ]
33
+ });
34
+ ```
35
+
36
+ ## Interface Token
37
+
38
+ Bind an interface to a concrete implementation using `useInterface<T>()`.
39
+
40
+ ```typescript
41
+ import { useInterface } from '@djodjonx/neo-syringe';
42
+
43
+ interface ICache {
44
+ get(key: string): any;
45
+ set(key: string, value: any): void;
46
+ }
47
+
48
+ class RedisCache implements ICache {
49
+ get(key: string) { /* ... */ }
50
+ set(key: string, value: any) { /* ... */ }
51
+ }
52
+
53
+ class MemoryCache implements ICache {
54
+ private store = new Map();
55
+ get(key: string) { return this.store.get(key); }
56
+ set(key: string, value: any) { this.store.set(key, value); }
57
+ }
58
+
59
+ // Production
60
+ const prodContainer = defineBuilderConfig({
61
+ injections: [
62
+ { token: useInterface<ICache>(), provider: RedisCache }
63
+ ]
64
+ });
65
+
66
+ // Development
67
+ const devContainer = defineBuilderConfig({
68
+ injections: [
69
+ { token: useInterface<ICache>(), provider: MemoryCache }
70
+ ]
71
+ });
72
+ ```
73
+
74
+ ### How It Works
75
+
76
+ At compile-time, `useInterface<ICache>()` is replaced with a unique string ID:
77
+
78
+ ```typescript
79
+ // Before (your code)
80
+ { token: useInterface<ICache>(), provider: RedisCache }
81
+
82
+ // After (generated)
83
+ { token: "ICache", provider: RedisCache }
84
+ ```
85
+
86
+ ## Explicit Provider
87
+
88
+ Override which class is used when resolving a token:
89
+
90
+ ```typescript
91
+ class UserService {
92
+ validate(user: User) { /* production logic */ }
93
+ }
94
+
95
+ class TestUserService extends UserService {
96
+ validate(user: User) { return true; } // Skip validation in tests
97
+ }
98
+
99
+ // Test container
100
+ const testContainer = defineBuilderConfig({
101
+ injections: [
102
+ { token: UserService, provider: TestUserService }
103
+ ]
104
+ });
105
+
106
+ const service = testContainer.resolve(UserService);
107
+ // service instanceof TestUserService === true
108
+ ```
109
+
110
+ ## Factory Provider
111
+
112
+ Use factory functions when you need:
113
+ - Dynamic configuration
114
+ - Container access for conditional resolution
115
+ - External resource initialization
116
+
117
+ ### Arrow Function (Auto-detected)
118
+
119
+ ```typescript
120
+ {
121
+ token: useInterface<IConfig>(),
122
+ provider: () => ({
123
+ apiUrl: process.env.API_URL,
124
+ environment: process.env.NODE_ENV
125
+ })
126
+ }
127
+ ```
128
+
129
+ ### Factory with Container Access
130
+
131
+ ```typescript
132
+ {
133
+ token: useInterface<IHttpClient>(),
134
+ provider: (container) => {
135
+ const config = container.resolve(useInterface<IConfig>());
136
+ const logger = container.resolve(useInterface<ILogger>());
137
+
138
+ return new HttpClient({
139
+ baseUrl: config.apiUrl,
140
+ logger,
141
+ timeout: config.timeout
142
+ });
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### Explicit Factory Flag
148
+
149
+ For regular functions that aren't arrow functions:
150
+
151
+ ```typescript
152
+ function createDatabaseConnection(container: Container) {
153
+ const config = container.resolve(useInterface<IDbConfig>());
154
+ return new DatabaseConnection(config.connectionString);
155
+ }
156
+
157
+ {
158
+ token: useInterface<IDatabase>(),
159
+ provider: createDatabaseConnection,
160
+ useFactory: true // Required for non-arrow functions
161
+ }
162
+ ```
163
+
164
+ ## Property Token
165
+
166
+ Inject primitive values (string, number, boolean) into class constructors.
167
+
168
+ ### The Problem
169
+
170
+ How do you inject configuration values without polluting your classes?
171
+
172
+ ```typescript
173
+ // ❌ Coupled to DI
174
+ class ApiService {
175
+ constructor(@inject('API_URL') private apiUrl: string) {}
176
+ }
177
+
178
+ // ❌ Coupled to environment
179
+ class ApiService {
180
+ private apiUrl = process.env.API_URL;
181
+ }
182
+ ```
183
+
184
+ ### The Solution
185
+
186
+ Use `useProperty<T>(Class, 'paramName')`:
187
+
188
+ ```typescript
189
+ import { useProperty } from '@djodjonx/neo-syringe';
190
+
191
+ // ✅ Pure class
192
+ class ApiService {
193
+ constructor(
194
+ private apiUrl: string,
195
+ private timeout: number,
196
+ private retryCount: number
197
+ ) {}
198
+ }
199
+
200
+ // Property tokens
201
+ const apiUrl = useProperty<string>(ApiService, 'apiUrl');
202
+ const timeout = useProperty<number>(ApiService, 'timeout');
203
+ const retryCount = useProperty<number>(ApiService, 'retryCount');
204
+
205
+ export const container = defineBuilderConfig({
206
+ injections: [
207
+ { token: apiUrl, provider: () => 'https://api.example.com' },
208
+ { token: timeout, provider: () => 5000 },
209
+ { token: retryCount, provider: () => 3 },
210
+ { token: ApiService } // All primitives injected!
211
+ ]
212
+ });
213
+ ```
214
+
215
+ ### Scoped to Class
216
+
217
+ Property tokens are scoped to their class, avoiding collisions:
218
+
219
+ ```typescript
220
+ const serviceAUrl = useProperty<string>(ServiceA, 'url');
221
+ const serviceBUrl = useProperty<string>(ServiceB, 'url');
222
+
223
+ // serviceAUrl !== serviceBUrl (different tokens!)
224
+ ```
225
+
226
+ ### Validated by LSP
227
+
228
+ The IDE plugin validates property names:
229
+
230
+ ```typescript
231
+ const invalid = useProperty<string>(ApiService, 'invalidParam');
232
+ // 🔴 Error: Parameter 'invalidParam' does not exist in ApiService constructor
233
+ ```
234
+
235
+ ## Combined Example
236
+
237
+ ```typescript
238
+ import { defineBuilderConfig, useInterface, useProperty } from '@djodjonx/neo-syringe';
239
+
240
+ // Interfaces
241
+ interface ILogger { log(msg: string): void; }
242
+ interface IHttpClient { get(url: string): Promise<any>; }
243
+
244
+ // Implementations
245
+ class ConsoleLogger implements ILogger {
246
+ log(msg: string) { console.log(msg); }
247
+ }
248
+
249
+ // Pure service class
250
+ class ApiService {
251
+ constructor(
252
+ private logger: ILogger,
253
+ private http: IHttpClient,
254
+ private baseUrl: string,
255
+ private timeout: number
256
+ ) {}
257
+ }
258
+
259
+ // Tokens
260
+ const baseUrl = useProperty<string>(ApiService, 'baseUrl');
261
+ const timeout = useProperty<number>(ApiService, 'timeout');
262
+
263
+ export const container = defineBuilderConfig({
264
+ name: 'AppContainer',
265
+ injections: [
266
+ // Interface bindings
267
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
268
+
269
+ // Factory for complex initialization
270
+ {
271
+ token: useInterface<IHttpClient>(),
272
+ provider: (container) => {
273
+ const logger = container.resolve(useInterface<ILogger>());
274
+ return new HttpClient(logger);
275
+ }
276
+ },
277
+
278
+ // Primitive values
279
+ { token: baseUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
280
+ { token: timeout, provider: () => 5000 },
281
+
282
+ // Service with mixed dependencies
283
+ { token: ApiService }
284
+ ]
285
+ });
286
+ ```
287
+