@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
@@ -0,0 +1,267 @@
1
+ # Basic Usage
2
+
3
+ Learn the fundamental concepts and injection patterns in Neo-Syringe.
4
+
5
+ ## Container Configuration
6
+
7
+ All configuration is done through `defineBuilderConfig`:
8
+
9
+ ```typescript
10
+ import { defineBuilderConfig } from '@djodjonx/neo-syringe';
11
+
12
+ export const container = defineBuilderConfig({
13
+ name: 'MyContainer', // Optional: container name for debugging
14
+ injections: [ // Required: list of injections
15
+ // ... your injections
16
+ ],
17
+ extends: [], // Optional: inherit from partials
18
+ useContainer: undefined // Optional: parent container
19
+ });
20
+ ```
21
+
22
+ ## Injection Types
23
+
24
+ ### Class Token (Autowire)
25
+
26
+ The simplest form - register a class and let Neo-Syringe resolve its dependencies:
27
+
28
+ ```typescript
29
+ class UserRepository {
30
+ findAll() { return []; }
31
+ }
32
+
33
+ class UserService {
34
+ constructor(private repo: UserRepository) {}
35
+ }
36
+
37
+ export const container = defineBuilderConfig({
38
+ injections: [
39
+ { token: UserRepository },
40
+ { token: UserService } // Dependencies auto-resolved
41
+ ]
42
+ });
43
+ ```
44
+
45
+ ### Interface Token
46
+
47
+ Use `useInterface<T>()` to bind interfaces to implementations:
48
+
49
+ ```typescript
50
+ import { useInterface } from '@djodjonx/neo-syringe';
51
+
52
+ interface ILogger {
53
+ log(msg: string): void;
54
+ }
55
+
56
+ class ConsoleLogger implements ILogger {
57
+ log(msg: string) { console.log(msg); }
58
+ }
59
+
60
+ class FileLogger implements ILogger {
61
+ log(msg: string) { /* write to file */ }
62
+ }
63
+
64
+ export const container = defineBuilderConfig({
65
+ injections: [
66
+ { token: useInterface<ILogger>(), provider: ConsoleLogger }
67
+ ]
68
+ });
69
+
70
+ // Resolve by interface
71
+ const logger = container.resolve(useInterface<ILogger>());
72
+ ```
73
+
74
+ ::: tip Automatic ID Generation
75
+ `useInterface<ILogger>()` generates a unique string ID at compile-time. You don't need to manage Symbols manually!
76
+ :::
77
+
78
+ ### Explicit Provider
79
+
80
+ Override the default implementation:
81
+
82
+ ```typescript
83
+ class UserService {
84
+ // ...
85
+ }
86
+
87
+ class MockUserService extends UserService {
88
+ // Test implementation
89
+ }
90
+
91
+ export const container = defineBuilderConfig({
92
+ injections: [
93
+ { token: UserService, provider: MockUserService }
94
+ ]
95
+ });
96
+ ```
97
+
98
+ ### Factory Provider
99
+
100
+ Use factory functions for dynamic instantiation:
101
+
102
+ ```typescript
103
+ // Arrow functions are auto-detected as factories
104
+ {
105
+ token: useInterface<IConfig>(),
106
+ provider: (container) => ({
107
+ apiUrl: process.env.API_URL ?? 'http://localhost',
108
+ timeout: 5000
109
+ })
110
+ }
111
+
112
+ // Factory with dependencies
113
+ {
114
+ token: useInterface<IService>(),
115
+ provider: (container) => {
116
+ const logger = container.resolve(useInterface<ILogger>());
117
+ const config = container.resolve(useInterface<IConfig>());
118
+ return new MyService(logger, config);
119
+ }
120
+ }
121
+
122
+ // Explicit factory flag (for non-arrow functions)
123
+ {
124
+ token: useInterface<IDatabase>(),
125
+ provider: createDatabaseConnection,
126
+ useFactory: true
127
+ }
128
+ ```
129
+
130
+ ### Property Token
131
+
132
+ Inject primitive values (string, number, boolean) while keeping classes pure:
133
+
134
+ ```typescript
135
+ import { useProperty } from '@djodjonx/neo-syringe';
136
+
137
+ // Pure class - no DI imports!
138
+ class ApiService {
139
+ constructor(
140
+ private apiUrl: string,
141
+ private timeout: number,
142
+ private debug: boolean
143
+ ) {}
144
+ }
145
+
146
+ // Define property tokens
147
+ const apiUrl = useProperty<string>(ApiService, 'apiUrl');
148
+ const timeout = useProperty<number>(ApiService, 'timeout');
149
+ const debug = useProperty<boolean>(ApiService, 'debug');
150
+
151
+ export const container = defineBuilderConfig({
152
+ injections: [
153
+ { token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
154
+ { token: timeout, provider: () => 5000 },
155
+ { token: debug, provider: () => process.env.NODE_ENV === 'development' },
156
+ { token: ApiService } // Primitives auto-wired!
157
+ ]
158
+ });
159
+ ```
160
+
161
+ ::: info Type Safety
162
+ `useProperty(ApiService, 'apiUrl')` creates a unique token scoped to that specific class parameter. This means `useProperty(ServiceA, 'url')` ≠ `useProperty(ServiceB, 'url')`.
163
+ :::
164
+
165
+ ## Resolving Services
166
+
167
+ ```typescript
168
+ import { container } from './container';
169
+ import { UserService } from './services/user.service';
170
+ import { useInterface } from '@djodjonx/neo-syringe';
171
+ import type { ILogger } from './services/logger';
172
+
173
+ // Resolve by class
174
+ const userService = container.resolve(UserService);
175
+
176
+ // Resolve by interface
177
+ const logger = container.resolve(useInterface<ILogger>());
178
+ ```
179
+
180
+ ## Partials (Modular Configuration)
181
+
182
+ Split configuration into reusable modules:
183
+
184
+ ```typescript
185
+ // logging.partial.ts
186
+ import { definePartialConfig, useInterface } from '@djodjonx/neo-syringe';
187
+
188
+ export const loggingConfig = definePartialConfig({
189
+ injections: [
190
+ { token: useInterface<ILogger>(), provider: ConsoleLogger }
191
+ ]
192
+ });
193
+
194
+ // database.partial.ts
195
+ export const databaseConfig = definePartialConfig({
196
+ injections: [
197
+ { token: useInterface<IDatabase>(), provider: PostgresDatabase }
198
+ ]
199
+ });
200
+
201
+ // container.ts
202
+ export const container = defineBuilderConfig({
203
+ extends: [loggingConfig, databaseConfig], // Inherit injections
204
+ injections: [
205
+ { token: UserService },
206
+ { token: OrderService }
207
+ ]
208
+ });
209
+ ```
210
+
211
+ ## Complete Example
212
+
213
+ ```typescript
214
+ // interfaces.ts
215
+ export interface ILogger {
216
+ log(msg: string): void;
217
+ }
218
+
219
+ export interface IUserRepository {
220
+ findById(id: string): User | null;
221
+ }
222
+
223
+ // implementations.ts
224
+ export class ConsoleLogger implements ILogger {
225
+ log(msg: string) { console.log(`[LOG] ${msg}`); }
226
+ }
227
+
228
+ export class InMemoryUserRepository implements IUserRepository {
229
+ private users = new Map<string, User>();
230
+
231
+ findById(id: string) {
232
+ return this.users.get(id) ?? null;
233
+ }
234
+ }
235
+
236
+ // services.ts
237
+ export class UserService {
238
+ constructor(
239
+ private logger: ILogger,
240
+ private repo: IUserRepository
241
+ ) {}
242
+
243
+ getUser(id: string) {
244
+ this.logger.log(`Fetching user ${id}`);
245
+ return this.repo.findById(id);
246
+ }
247
+ }
248
+
249
+ // container.ts
250
+ import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
251
+
252
+ export const container = defineBuilderConfig({
253
+ name: 'AppContainer',
254
+ injections: [
255
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
256
+ { token: useInterface<IUserRepository>(), provider: InMemoryUserRepository },
257
+ { token: UserService }
258
+ ]
259
+ });
260
+
261
+ // main.ts
262
+ import { container } from './container';
263
+
264
+ const userService = container.resolve(UserService);
265
+ const user = userService.getUser('123');
266
+ ```
267
+
@@ -0,0 +1,174 @@
1
+ # CLI Validator
2
+
3
+ Validate your dependency graph in CI/CD pipelines.
4
+
5
+ ## Installation
6
+
7
+ The CLI is included with the main package:
8
+
9
+ ```bash
10
+ npx neo-syringe
11
+ # or
12
+ pnpm exec neo-syringe
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ Run in your project root (where `tsconfig.json` is located):
18
+
19
+ ```bash
20
+ neo-syringe
21
+ ```
22
+
23
+ ### Options
24
+
25
+ ```bash
26
+ neo-syringe [options]
27
+
28
+ Options:
29
+ -p, --project <path> Path to tsconfig.json (default: "./tsconfig.json")
30
+ -h, --help Display help
31
+ -v, --version Display version
32
+ ```
33
+
34
+ ## Output
35
+
36
+ ### Success
37
+
38
+ ```
39
+ 🔍 Analyzing project: /path/to/tsconfig.json
40
+ Found 45 services.
41
+ 🛡️ Validating graph...
42
+ ✅ Validation passed! No circular dependencies or missing bindings found.
43
+ ```
44
+
45
+ ### Circular Dependency Detected
46
+
47
+ ```
48
+ 🔍 Analyzing project: /path/to/tsconfig.json
49
+ Found 12 services.
50
+ 🛡️ Validating graph...
51
+ ❌ Validation failed!
52
+
53
+ Error: Circular dependency detected: A -> B -> C -> A
54
+ ```
55
+
56
+ ### Missing Binding
57
+
58
+ ```
59
+ 🔍 Analyzing project: /path/to/tsconfig.json
60
+ Found 8 services.
61
+ 🛡️ Validating graph...
62
+ ❌ Validation failed!
63
+
64
+ Error: Missing binding: 'UserService' depends on 'ILogger', but no provider registered.
65
+ ```
66
+
67
+ ### Duplicate Registration
68
+
69
+ ```
70
+ 🔍 Analyzing project: /path/to/tsconfig.json
71
+ Found 15 services.
72
+ 🛡️ Validating graph...
73
+ ❌ Validation failed!
74
+
75
+ Error: Duplicate registration: 'ILogger' is already registered in the parent container.
76
+ Use 'scoped: true' to override the parent's registration intentionally.
77
+ ```
78
+
79
+ ## Exit Codes
80
+
81
+ | Code | Meaning |
82
+ |------|---------|
83
+ | 0 | Validation passed |
84
+ | 1 | Validation failed |
85
+ | 2 | Configuration error |
86
+
87
+ ## CI/CD Integration
88
+
89
+ ### GitHub Actions
90
+
91
+ ```yaml
92
+ # .github/workflows/ci.yml
93
+ name: CI
94
+
95
+ on: [push, pull_request]
96
+
97
+ jobs:
98
+ validate:
99
+ runs-on: ubuntu-latest
100
+ steps:
101
+ - uses: actions/checkout@v4
102
+
103
+ - uses: pnpm/action-setup@v4
104
+
105
+ - uses: actions/setup-node@v4
106
+ with:
107
+ node-version: '20'
108
+ cache: 'pnpm'
109
+
110
+ - run: pnpm install --frozen-lockfile
111
+
112
+ - name: Validate DI Graph
113
+ run: pnpm exec neo-syringe
114
+ ```
115
+
116
+ ### GitLab CI
117
+
118
+ ```yaml
119
+ # .gitlab-ci.yml
120
+ validate:
121
+ stage: test
122
+ script:
123
+ - pnpm install
124
+ - pnpm exec neo-syringe
125
+ ```
126
+
127
+ ### npm scripts
128
+
129
+ Add to `package.json`:
130
+
131
+ ```json
132
+ {
133
+ "scripts": {
134
+ "validate": "neo-syringe",
135
+ "prebuild": "neo-syringe",
136
+ "ci": "pnpm lint && pnpm validate && pnpm test"
137
+ }
138
+ }
139
+ ```
140
+
141
+ ## Best Practices
142
+
143
+ ### Run Before Build
144
+
145
+ Validate before building to catch errors early:
146
+
147
+ ```json
148
+ {
149
+ "scripts": {
150
+ "prebuild": "neo-syringe",
151
+ "build": "vite build"
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Run in PR Checks
157
+
158
+ Add validation to your PR workflow:
159
+
160
+ ```yaml
161
+ - name: Validate Dependencies
162
+ run: pnpm exec neo-syringe
163
+ # Fails the PR if validation fails
164
+ ```
165
+
166
+ ### Use with Husky
167
+
168
+ Validate on pre-push:
169
+
170
+ ```bash
171
+ # .husky/pre-push
172
+ pnpm exec neo-syringe
173
+ ```
174
+
@@ -0,0 +1,284 @@
1
+ # Generated Code
2
+
3
+ Understand what the compiler produces.
4
+
5
+ ## Overview
6
+
7
+ Neo-Syringe transforms your configuration into optimized TypeScript at build time. This page shows what your code looks like before and after compilation.
8
+
9
+ ## Before (Your Configuration)
10
+
11
+ ```typescript
12
+ // container.ts
13
+ import { defineBuilderConfig, useInterface, useProperty } from '@djodjonx/neo-syringe';
14
+
15
+ interface ILogger {
16
+ log(msg: string): void;
17
+ }
18
+
19
+ class ConsoleLogger implements ILogger {
20
+ log(msg: string) { console.log(msg); }
21
+ }
22
+
23
+ class ApiService {
24
+ constructor(
25
+ private logger: ILogger,
26
+ private apiUrl: string
27
+ ) {}
28
+ }
29
+
30
+ const apiUrl = useProperty<string>(ApiService, 'apiUrl');
31
+
32
+ export const container = defineBuilderConfig({
33
+ name: 'AppContainer',
34
+ injections: [
35
+ { token: useInterface<ILogger>(), provider: ConsoleLogger },
36
+ { token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
37
+ { token: ApiService }
38
+ ]
39
+ });
40
+ ```
41
+
42
+ ## After (Generated Code)
43
+
44
+ The build plugin replaces the entire file:
45
+
46
+ ```typescript
47
+ // container.ts (after build)
48
+ import * as Import_0 from './container';
49
+
50
+ // -- Factories --
51
+
52
+ function create_ILogger(container: NeoContainer) {
53
+ return new Import_0.ConsoleLogger();
54
+ }
55
+
56
+ function create_ApiService_apiUrl(container: NeoContainer) {
57
+ const userFactory = () => process.env.API_URL ?? 'http://localhost';
58
+ return userFactory(container);
59
+ }
60
+
61
+ function create_ApiService(container: NeoContainer) {
62
+ return new Import_0.ApiService(
63
+ container.resolve("ILogger"),
64
+ container.resolve("PropertyToken:ApiService.apiUrl")
65
+ );
66
+ }
67
+
68
+ // -- Container --
69
+
70
+ export class NeoContainer {
71
+ private instances = new Map<any, any>();
72
+
73
+ constructor(
74
+ private parent?: any,
75
+ private legacy?: any[],
76
+ private name: string = 'AppContainer'
77
+ ) {}
78
+
79
+ public resolve(token: any): any {
80
+ // 1. Try local resolution
81
+ const result = this.resolveLocal(token);
82
+ if (result !== undefined) return result;
83
+
84
+ // 2. Delegate to parent
85
+ if (this.parent) {
86
+ try {
87
+ return this.parent.resolve(token);
88
+ } catch (e) {
89
+ // Continue to legacy
90
+ }
91
+ }
92
+
93
+ // 3. Delegate to legacy containers
94
+ if (this.legacy) {
95
+ for (const legacyContainer of this.legacy) {
96
+ try {
97
+ if (legacyContainer.resolve) {
98
+ return legacyContainer.resolve(token);
99
+ }
100
+ } catch (e) {
101
+ // Try next
102
+ }
103
+ }
104
+ }
105
+
106
+ throw new Error(`[${this.name}] Service not found: ${token}`);
107
+ }
108
+
109
+ private resolveLocal(token: any): any {
110
+ // Interface token (string-based)
111
+ if (token === "ILogger") {
112
+ if (!this.instances.has("ILogger")) {
113
+ this.instances.set("ILogger", create_ILogger(this));
114
+ }
115
+ return this.instances.get("ILogger");
116
+ }
117
+
118
+ // Property token (string-based)
119
+ if (token === "PropertyToken:ApiService.apiUrl") {
120
+ if (!this.instances.has("PropertyToken:ApiService.apiUrl")) {
121
+ this.instances.set("PropertyToken:ApiService.apiUrl", create_ApiService_apiUrl(this));
122
+ }
123
+ return this.instances.get("PropertyToken:ApiService.apiUrl");
124
+ }
125
+
126
+ // Class token (reference-based)
127
+ if (token === Import_0.ApiService) {
128
+ if (!this.instances.has(Import_0.ApiService)) {
129
+ this.instances.set(Import_0.ApiService, create_ApiService(this));
130
+ }
131
+ return this.instances.get(Import_0.ApiService);
132
+ }
133
+
134
+ return undefined;
135
+ }
136
+
137
+ public createChildContainer(): NeoContainer {
138
+ return new NeoContainer(this, this.legacy, `Child of ${this.name}`);
139
+ }
140
+
141
+ // For debugging
142
+ public get _graph() {
143
+ return ["ILogger", "PropertyToken:ApiService.apiUrl", "ApiService"];
144
+ }
145
+ }
146
+
147
+ export const container = new NeoContainer();
148
+ ```
149
+
150
+ ## Key Observations
151
+
152
+ ### Token Resolution
153
+
154
+ | Token Type | Resolution Method | Example |
155
+ |------------|-------------------|---------|
156
+ | Interface | String comparison | `token === "ILogger"` |
157
+ | Class | Reference comparison | `token === Import_0.ApiService` |
158
+ | Property | String comparison | `token === "PropertyToken:ApiService.apiUrl"` |
159
+
160
+ ### Singleton Pattern
161
+
162
+ Singletons use the `instances` Map:
163
+
164
+ ```typescript
165
+ if (!this.instances.has("ILogger")) {
166
+ this.instances.set("ILogger", create_ILogger(this));
167
+ }
168
+ return this.instances.get("ILogger");
169
+ ```
170
+
171
+ ### Transient Pattern
172
+
173
+ Transients return directly without caching:
174
+
175
+ ```typescript
176
+ if (token === "RequestContext") {
177
+ return create_RequestContext(this); // No caching!
178
+ }
179
+ ```
180
+
181
+ ### Dependency Injection
182
+
183
+ Dependencies are resolved recursively:
184
+
185
+ ```typescript
186
+ function create_ApiService(container: NeoContainer) {
187
+ return new Import_0.ApiService(
188
+ container.resolve("ILogger"), // Resolves ILogger first
189
+ container.resolve("PropertyToken:...") // Resolves property
190
+ );
191
+ }
192
+ ```
193
+
194
+ ## With Parent Container
195
+
196
+ When using `useContainer`:
197
+
198
+ ```typescript
199
+ // Configuration
200
+ const child = defineBuilderConfig({
201
+ useContainer: parent,
202
+ injections: [{ token: UserService }]
203
+ });
204
+ ```
205
+
206
+ ```typescript
207
+ // Generated
208
+ import { parent } from './parent-container';
209
+
210
+ export class NeoContainer {
211
+ constructor(
212
+ private parent: typeof parent = parent, // 👈 Parent reference
213
+ // ...
214
+ ) {}
215
+
216
+ resolve(token: any): any {
217
+ const local = this.resolveLocal(token);
218
+ if (local !== undefined) return local;
219
+
220
+ // Delegate to parent
221
+ if (this.parent) {
222
+ return this.parent.resolve(token);
223
+ }
224
+
225
+ throw new Error('...');
226
+ }
227
+ }
228
+
229
+ export const child = new NeoContainer(parent);
230
+ ```
231
+
232
+ ## With Factory Provider
233
+
234
+ ```typescript
235
+ // Configuration
236
+ {
237
+ token: useInterface<IDatabase>(),
238
+ provider: (container) => {
239
+ const config = container.resolve(useInterface<IConfig>());
240
+ return new PostgresDatabase(config.connectionString);
241
+ }
242
+ }
243
+ ```
244
+
245
+ ```typescript
246
+ // Generated
247
+ function create_IDatabase(container: NeoContainer) {
248
+ const userFactory = (container) => {
249
+ const config = container.resolve("IConfig");
250
+ return new PostgresDatabase(config.connectionString);
251
+ };
252
+ return userFactory(container);
253
+ }
254
+ ```
255
+
256
+ ## Bundle Size Impact
257
+
258
+ | Aspect | Traditional DI | Neo-Syringe |
259
+ |--------|---------------|-------------|
260
+ | Container library | 4-11 KB | 0 KB |
261
+ | reflect-metadata | ~3 KB | 0 KB |
262
+ | Generated code | N/A | ~50-200 lines |
263
+ | **Total** | **7-14 KB** | **< 1 KB** |
264
+
265
+ The generated code is:
266
+ - ✅ Tree-shakeable (unused services removed)
267
+ - ✅ Minifiable (standard JavaScript)
268
+ - ✅ No external dependencies
269
+
270
+ ## Debugging
271
+
272
+ The generated container includes a `_graph` getter for inspection:
273
+
274
+ ```typescript
275
+ console.log(container._graph);
276
+ // ["ILogger", "PropertyToken:ApiService.apiUrl", "ApiService"]
277
+ ```
278
+
279
+ Each container also has a `name` for error messages:
280
+
281
+ ```typescript
282
+ // Error: [AppContainer] Service not found: UnknownToken
283
+ ```
284
+