@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/README.md
CHANGED
|
@@ -5,821 +5,142 @@
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://www.npmjs.com/package/@djodjonx/neo-syringe"><img src="https://img.shields.io/npm/v/@djodjonx/neo-syringe.svg?style=flat-square" alt="npm version"></a>
|
|
7
7
|
<a href="https://github.com/djodjonx/neo-syringe/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/djodjonx/neo-syringe/ci.yml?style=flat-square&label=tests" alt="Tests"></a>
|
|
8
|
-
<a href="https://github.com/djodjonx/neo-syringe/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/djodjonx/neo-syringe/ci.yml?style=flat-square" alt="CI"></a>
|
|
9
8
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.0+-blue.svg?style=flat-square" alt="TypeScript"></a>
|
|
10
9
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square" alt="License: MIT"></a>
|
|
10
|
+
<a href="https://djodjonx.github.io/neo-syringe/"><img src="https://img.shields.io/badge/docs-VitePress-0d9488.svg?style=flat-square" alt="Documentation"></a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<h1 align="center">Zero-Overhead, Compile-Time Dependency Injection</h1>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
<strong>Neo-Syringe</strong>
|
|
16
|
+
<strong>Neo-Syringe</strong> shifts DI resolution from <strong>Runtime</strong> to <strong>Build-Time</strong>.<br>
|
|
17
|
+
No reflection, no decorators, just pure TypeScript.
|
|
17
18
|
</p>
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- ✨ **Use Interfaces as Tokens**: Native support for `useInterface<ILogger>()` without manual Symbols
|
|
24
|
-
- 🚀 **Zero Runtime Overhead**: No reflection, no `reflect-metadata`, just pure factory functions
|
|
25
|
-
- 🛡️ **Compile-Time Safety**: Detect circular dependencies, missing bindings, and type mismatches instantly
|
|
26
|
-
- 🔄 **Migrate Gradually**: Use `useContainer` to bridge existing containers (like `tsyringe`)
|
|
27
|
-
- 🤖 **Validate in CI**: Standalone CLI to verify your dependency graph before deployment
|
|
28
|
-
|
|
29
|
-
## 📚 Documentation
|
|
30
|
-
|
|
31
|
-
- **[Quick Start](#quick-start)** - Wire your first application in 5 minutes
|
|
32
|
-
- **[Injection Types](#injection-types)** - All ways to define dependencies
|
|
33
|
-
- **[Generated Code](#generated-code-example)** - See what the compiler produces
|
|
34
|
-
- **[Parent Container](#parent-container-usecontainer)** - SharedKernel and modular architecture
|
|
35
|
-
- **[Legacy Migration](#legacy-migration-usecontainer-1)** - Bridge existing containers (tsyringe, etc.)
|
|
36
|
-
- **[CLI Validator](#cli-validator)** - Ensure graph integrity in CI/CD
|
|
37
|
-
- **[IDE Support](#ide-plugin-for-real-time-validation)** - Get real-time errors in VS Code
|
|
38
|
-
|
|
39
|
-
## Why Neo-Syringe?
|
|
40
|
-
|
|
41
|
-
Traditional containers (InversifyJS, tsyringe) or modern wrappers (WireDI) rely on **Runtime Resolution**. This means:
|
|
42
|
-
|
|
43
|
-
- ❌ You ship the DI container logic to the browser
|
|
44
|
-
- ❌ Errors (missing bindings) happen at runtime
|
|
45
|
-
- ❌ Interfaces are erased, requiring manual Symbols
|
|
46
|
-
|
|
47
|
-
**Neo-Syringe is different.** It works as a **Compiler Plugin**:
|
|
48
|
-
|
|
49
|
-
### 1. Code Generation
|
|
50
|
-
Neo-Syringe analyzes your configuration and generates a specialized TypeScript class with hardcoded `new Service(new Dep())` calls. **Zero runtime resolution overhead**.
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://djodjonx.github.io/neo-syringe/"><strong>📚 Read the Documentation →</strong></a>
|
|
22
|
+
</p>
|
|
51
23
|
|
|
52
|
-
|
|
53
|
-
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="https://djodjonx.github.io/neo-syringe/guide/getting-started">Getting Started</a> •
|
|
26
|
+
<a href="https://djodjonx.github.io/neo-syringe/guide/why-neo-syringe">Why Neo-Syringe?</a> •
|
|
27
|
+
<a href="https://djodjonx.github.io/neo-syringe/api/types">API Reference</a>
|
|
28
|
+
</p>
|
|
54
29
|
|
|
55
|
-
|
|
56
|
-
Your business classes stay **100% pure** - no decorators, no DI imports, no framework coupling.
|
|
30
|
+
---
|
|
57
31
|
|
|
58
|
-
|
|
59
|
-
Errors are detected **in your IDE** instantly:
|
|
32
|
+
## ✨ Features
|
|
60
33
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
})
|
|
68
|
-
```
|
|
34
|
+
- **Use Interfaces as Tokens** - `useInterface<ILogger>()` without manual Symbols
|
|
35
|
+
- **Zero Runtime Overhead** - Generated factory functions, no DI library shipped
|
|
36
|
+
- **Compile-Time Safety** - Errors detected in your IDE, not at runtime
|
|
37
|
+
- **Pure Classes** - No decorators, no DI imports in your business code
|
|
38
|
+
- **Gradual Migration** - Bridge existing containers (tsyringe, InversifyJS)
|
|
39
|
+
- **CI Validation** - CLI to verify your dependency graph
|
|
69
40
|
|
|
70
|
-
## Installation
|
|
41
|
+
## 📦 Installation
|
|
71
42
|
|
|
72
43
|
```bash
|
|
73
|
-
#
|
|
44
|
+
# npm
|
|
74
45
|
npm install @djodjonx/neo-syringe
|
|
75
46
|
npm install -D unplugin
|
|
76
47
|
|
|
77
|
-
#
|
|
48
|
+
# pnpm
|
|
78
49
|
pnpm add @djodjonx/neo-syringe
|
|
79
50
|
pnpm add -D unplugin
|
|
80
51
|
```
|
|
81
52
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
### Build System Integration
|
|
85
|
-
|
|
86
|
-
Configure the plugin in your bundler to enable compile-time code generation.
|
|
87
|
-
|
|
88
|
-
<details>
|
|
89
|
-
<summary><strong>Vite</strong></summary>
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
// vite.config.ts
|
|
93
|
-
import { defineConfig } from 'vite';
|
|
94
|
-
import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
|
|
95
|
-
|
|
96
|
-
export default defineConfig({
|
|
97
|
-
plugins: [neoSyringePlugin.vite()]
|
|
98
|
-
});
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
</details>
|
|
102
|
-
|
|
103
|
-
<details>
|
|
104
|
-
<summary><strong>Rollup</strong></summary>
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// rollup.config.js
|
|
108
|
-
import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
|
|
109
|
-
|
|
110
|
-
export default {
|
|
111
|
-
plugins: [neoSyringePlugin.rollup()]
|
|
112
|
-
};
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
</details>
|
|
116
|
-
|
|
117
|
-
<details>
|
|
118
|
-
<summary><strong>Webpack</strong></summary>
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
// webpack.config.js
|
|
122
|
-
module.exports = {
|
|
123
|
-
plugins: [require('@djodjonx/neo-syringe/plugin').webpack()]
|
|
124
|
-
};
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
</details>
|
|
128
|
-
|
|
129
|
-
### Dev Mode & HMR Support
|
|
130
|
-
|
|
131
|
-
✅ **The plugin works in dev mode** (Vite, Rollup watch, Webpack dev server).
|
|
132
|
-
|
|
133
|
-
The `transform` hook is called on every file change, so your container is regenerated instantly during development with full Hot Module Replacement (HMR) support.
|
|
134
|
-
|
|
135
|
-
> ⚠️ **Best Practice**: Put your container configuration in a **dedicated file** (e.g., `container.ts`). The plugin replaces the entire file content with generated code, so mixing container config with other exports may cause issues.
|
|
136
|
-
|
|
137
|
-
```
|
|
138
|
-
src/
|
|
139
|
-
├── container.ts # ✅ Dedicated file for defineBuilderConfig
|
|
140
|
-
├── services/
|
|
141
|
-
│ ├── logger.ts
|
|
142
|
-
│ └── user.service.ts
|
|
143
|
-
└── main.ts
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## Quick Start
|
|
147
|
-
|
|
148
|
-
### 1. Define Services (Pure TypeScript)
|
|
149
|
-
|
|
150
|
-
No decorators required. Just plain classes and interfaces.
|
|
53
|
+
## 🚀 Quick Example
|
|
151
54
|
|
|
152
55
|
```typescript
|
|
153
|
-
//
|
|
154
|
-
|
|
56
|
+
// Pure TypeScript - no decorators!
|
|
57
|
+
interface ILogger {
|
|
155
58
|
log(msg: string): void;
|
|
156
59
|
}
|
|
157
60
|
|
|
158
|
-
|
|
61
|
+
class ConsoleLogger implements ILogger {
|
|
159
62
|
log(msg: string) { console.log(msg); }
|
|
160
63
|
}
|
|
161
64
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
constructor(private logger: ILogger) {} // Dependency is an Interface!
|
|
65
|
+
class UserService {
|
|
66
|
+
constructor(private logger: ILogger) {}
|
|
165
67
|
}
|
|
166
68
|
```
|
|
167
69
|
|
|
168
|
-
### 2. Configure the Container
|
|
169
|
-
|
|
170
|
-
Use `defineBuilderConfig` to wire your graph.
|
|
171
|
-
|
|
172
70
|
```typescript
|
|
173
|
-
//
|
|
71
|
+
// container.ts
|
|
174
72
|
import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
|
|
175
|
-
import { ILogger, ConsoleLogger } from './logger';
|
|
176
|
-
import { UserService } from './user.service';
|
|
177
|
-
|
|
178
|
-
export const appConfig = defineBuilderConfig({
|
|
179
|
-
name: 'AppModule',
|
|
180
|
-
injections: [
|
|
181
|
-
// Bind Interface -> Implementation
|
|
182
|
-
{ token: useInterface<ILogger>(), provider: ConsoleLogger },
|
|
183
|
-
|
|
184
|
-
// Autowire Class
|
|
185
|
-
{ token: UserService }
|
|
186
|
-
]
|
|
187
|
-
});
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### 3. Use the Container
|
|
191
|
-
|
|
192
|
-
The compiler plugin replaces the configuration with a generated container class.
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
// main.ts
|
|
196
|
-
import { appConfig } from './config';
|
|
197
|
-
|
|
198
|
-
const container = appConfig; // This IS the container at runtime!
|
|
199
|
-
const userService = container.resolve(UserService);
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## Injection Types
|
|
205
|
-
|
|
206
|
-
### Class Token (Autowire)
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
{ token: UserService }
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### Interface Token
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
{ token: useInterface<ILogger>(), provider: ConsoleLogger }
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### Explicit Provider
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
{ token: UserService, provider: MockUserService }
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Factory Provider
|
|
225
|
-
|
|
226
|
-
Use factory functions for dynamic instantiation or container access.
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
// Auto-detected: arrow functions are treated as factories
|
|
230
|
-
{
|
|
231
|
-
token: useInterface<IConfig>(),
|
|
232
|
-
provider: (container) => ({
|
|
233
|
-
apiUrl: process.env.API_URL ?? 'http://localhost',
|
|
234
|
-
timeout: 5000
|
|
235
|
-
})
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Explicit factory flag
|
|
239
|
-
{
|
|
240
|
-
token: useInterface<IDatabase>(),
|
|
241
|
-
provider: createDatabaseConnection,
|
|
242
|
-
useFactory: true
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Factory with dependencies
|
|
246
|
-
{
|
|
247
|
-
token: useInterface<IService>(),
|
|
248
|
-
provider: (container) => {
|
|
249
|
-
const logger = container.resolve(useInterface<ILogger>());
|
|
250
|
-
return new MyService(logger);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### Primitive Values with `useProperty`
|
|
256
|
-
|
|
257
|
-
Inject primitives (string, number, boolean) while keeping classes **pure**.
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
import { defineBuilderConfig, useProperty } from '@djodjonx/neo-syringe';
|
|
261
|
-
|
|
262
|
-
// Pure class - no DI imports!
|
|
263
|
-
class ApiService {
|
|
264
|
-
constructor(
|
|
265
|
-
private apiUrl: string,
|
|
266
|
-
private maxRetries: number
|
|
267
|
-
) {}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Define property tokens
|
|
271
|
-
const apiUrl = useProperty<string>(ApiService, 'apiUrl');
|
|
272
|
-
const maxRetries = useProperty<number>(ApiService, 'maxRetries');
|
|
273
|
-
|
|
274
|
-
export const config = defineBuilderConfig({
|
|
275
|
-
injections: [
|
|
276
|
-
{ token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
|
|
277
|
-
{ token: maxRetries, provider: () => 5 },
|
|
278
|
-
{ token: ApiService } // Auto-wires primitives!
|
|
279
|
-
]
|
|
280
|
-
});
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
**Benefits:**
|
|
284
|
-
- ✅ No collision: `useProperty(ApiService, 'url')` ≠ `useProperty(AuthService, 'url')`
|
|
285
|
-
- ✅ LSP validation: Detects if parameter doesn't exist
|
|
286
|
-
- ✅ Refactoring-friendly: Rename parameter → LSP error
|
|
287
|
-
|
|
288
|
-
### Lifecycle
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
{ token: UserService, lifecycle: 'transient' } // Default is 'singleton'
|
|
292
|
-
|
|
293
|
-
// Factories support lifecycle too
|
|
294
|
-
{
|
|
295
|
-
token: useInterface<IRequest>(),
|
|
296
|
-
provider: () => ({ id: crypto.randomUUID() }),
|
|
297
|
-
lifecycle: 'transient'
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Scoped Injections (`scoped: true`)
|
|
302
|
-
|
|
303
|
-
Override a token from a parent container **without causing a duplicate error**. Useful for testing or module-specific overrides.
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
// SharedKernel - Production logger
|
|
307
|
-
const sharedKernel = defineBuilderConfig({
|
|
308
|
-
name: 'SharedKernel',
|
|
309
|
-
injections: [
|
|
310
|
-
{ token: useInterface<ILogger>(), provider: ConsoleLogger, lifecycle: 'singleton' }
|
|
311
|
-
]
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// TestModule - Override with MockLogger, scoped to this container
|
|
315
|
-
const testModule = defineBuilderConfig({
|
|
316
|
-
name: 'TestModule',
|
|
317
|
-
useContainer: sharedKernel,
|
|
318
|
-
injections: [
|
|
319
|
-
{
|
|
320
|
-
token: useInterface<ILogger>(),
|
|
321
|
-
provider: MockLogger,
|
|
322
|
-
lifecycle: 'transient', // Can use different lifecycle than parent
|
|
323
|
-
scoped: true // 👈 Allows override without duplicate error
|
|
324
|
-
},
|
|
325
|
-
{ token: UserService } // Uses MockLogger from this container
|
|
326
|
-
]
|
|
327
|
-
});
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
**Behavior:**
|
|
331
|
-
|
|
332
|
-
| Scenario | Without `scoped` | With `scoped: true` |
|
|
333
|
-
|----------|------------------|---------------------|
|
|
334
|
-
| Token exists in parent | ❌ Duplicate error | ✅ Override locally |
|
|
335
|
-
| Token does not exist in parent | ✅ OK | ✅ OK |
|
|
336
|
-
| Resolution | Delegates to parent | Uses local instance |
|
|
337
|
-
|
|
338
|
-
**Use Cases:**
|
|
339
|
-
- 🧪 **Testing**: Override production services with mocks
|
|
340
|
-
- 🔧 **Module isolation**: Each module has its own instance
|
|
341
|
-
- ⚙️ **Different lifecycle**: Parent uses singleton, child uses transient
|
|
342
|
-
|
|
343
|
-
---
|
|
344
|
-
|
|
345
|
-
## Generated Code Example
|
|
346
|
-
|
|
347
|
-
To understand how Neo-Syringe works, here's what your code looks like **before** and **after** compilation.
|
|
348
|
-
|
|
349
|
-
### Before (Your Configuration)
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
// config.ts
|
|
353
|
-
import { defineBuilderConfig, useInterface, useProperty } from '@djodjonx/neo-syringe';
|
|
354
|
-
|
|
355
|
-
interface ILogger { log(msg: string): void; }
|
|
356
|
-
class ConsoleLogger implements ILogger {
|
|
357
|
-
log(msg: string) { console.log(msg); }
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
class ApiService {
|
|
361
|
-
constructor(
|
|
362
|
-
private logger: ILogger,
|
|
363
|
-
private apiUrl: string
|
|
364
|
-
) {}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const apiUrl = useProperty<string>(ApiService, 'apiUrl');
|
|
368
73
|
|
|
369
74
|
export const container = defineBuilderConfig({
|
|
370
|
-
name: 'AppContainer',
|
|
371
|
-
injections: [
|
|
372
|
-
{ token: useInterface<ILogger>(), provider: ConsoleLogger },
|
|
373
|
-
{ token: apiUrl, provider: () => process.env.API_URL ?? 'http://localhost' },
|
|
374
|
-
{ token: ApiService }
|
|
375
|
-
]
|
|
376
|
-
});
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### After (Generated Code)
|
|
380
|
-
|
|
381
|
-
The build plugin replaces `defineBuilderConfig(...)` with an optimized container class:
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
// config.ts (after build)
|
|
385
|
-
import * as Import_0 from './services';
|
|
386
|
-
|
|
387
|
-
// -- Factories (generated) --
|
|
388
|
-
function create_ILogger(container: NeoContainer) {
|
|
389
|
-
return new Import_0.ConsoleLogger();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function create_ApiService_apiUrl(container: NeoContainer) {
|
|
393
|
-
const userFactory = () => process.env.API_URL ?? 'http://localhost';
|
|
394
|
-
return userFactory(container);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function create_ApiService(container: NeoContainer) {
|
|
398
|
-
return new Import_0.ApiService(
|
|
399
|
-
container.resolve("ILogger"),
|
|
400
|
-
container.resolve("PropertyToken:ApiService.apiUrl")
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// -- Container (generated) --
|
|
405
|
-
export class NeoContainer {
|
|
406
|
-
private instances = new Map<any, any>();
|
|
407
|
-
|
|
408
|
-
constructor(
|
|
409
|
-
private parent?: any,
|
|
410
|
-
private legacy?: any[],
|
|
411
|
-
private name: string = 'AppContainer'
|
|
412
|
-
) {}
|
|
413
|
-
|
|
414
|
-
public resolve(token: any): any {
|
|
415
|
-
const result = this.resolveLocal(token);
|
|
416
|
-
if (result !== undefined) return result;
|
|
417
|
-
|
|
418
|
-
if (this.parent) {
|
|
419
|
-
try { return this.parent.resolve(token); }
|
|
420
|
-
catch (e) { /* fallback */ }
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
throw new Error(`[${this.name}] Service not found: ${token}`);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
private resolveLocal(token: any): any {
|
|
427
|
-
// Interface token (string)
|
|
428
|
-
if (token === "ILogger") {
|
|
429
|
-
if (!this.instances.has("ILogger")) {
|
|
430
|
-
this.instances.set("ILogger", create_ILogger(this));
|
|
431
|
-
}
|
|
432
|
-
return this.instances.get("ILogger");
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// PropertyToken (string)
|
|
436
|
-
if (token === "PropertyToken:ApiService.apiUrl") {
|
|
437
|
-
if (!this.instances.has("PropertyToken:ApiService.apiUrl")) {
|
|
438
|
-
this.instances.set("PropertyToken:ApiService.apiUrl", create_ApiService_apiUrl(this));
|
|
439
|
-
}
|
|
440
|
-
return this.instances.get("PropertyToken:ApiService.apiUrl");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Class token (reference)
|
|
444
|
-
if (token === Import_0.ApiService) {
|
|
445
|
-
if (!this.instances.has(Import_0.ApiService)) {
|
|
446
|
-
this.instances.set(Import_0.ApiService, create_ApiService(this));
|
|
447
|
-
}
|
|
448
|
-
return this.instances.get(Import_0.ApiService);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return undefined;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export const container = new NeoContainer();
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
### Key Observations
|
|
459
|
-
|
|
460
|
-
| Aspect | Before | After |
|
|
461
|
-
|--------|--------|-------|
|
|
462
|
-
| **Resolution** | Runtime lookup | Direct `new` calls |
|
|
463
|
-
| **Interfaces** | Type-erased | String IDs generated |
|
|
464
|
-
| **Dependencies** | Analyzed at runtime | Hardcoded in factories |
|
|
465
|
-
| **Container size** | Full DI library | ~50 lines of code |
|
|
466
|
-
| **Performance** | Map lookups + reflection | Direct instantiation |
|
|
467
|
-
|
|
468
|
-
> 💡 **The generated code has zero dependencies** - no DI library shipped to production!
|
|
469
|
-
|
|
470
|
-
---
|
|
471
|
-
|
|
472
|
-
## Advanced Features
|
|
473
|
-
|
|
474
|
-
### Parent Container (`useContainer`)
|
|
475
|
-
|
|
476
|
-
Neo-Syringe supports **hierarchical containers**. A child container can delegate resolution to a parent container for tokens it doesn't define locally.
|
|
477
|
-
|
|
478
|
-
#### Example: SharedKernel Architecture
|
|
479
|
-
|
|
480
|
-
Perfect for modular applications where core services are shared across bounded contexts.
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
// shared-kernel/container.ts
|
|
484
|
-
import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
|
|
485
|
-
|
|
486
|
-
// Shared interfaces
|
|
487
|
-
export interface ILogger { log(msg: string): void; }
|
|
488
|
-
export interface IEventBus { publish(event: any): void; }
|
|
489
|
-
|
|
490
|
-
class ConsoleLogger implements ILogger {
|
|
491
|
-
log(msg: string) { console.log(`[LOG] ${msg}`); }
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
class InMemoryEventBus implements IEventBus {
|
|
495
|
-
publish(event: any) { console.log('Event:', event); }
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// SharedKernel container - reusable across modules
|
|
499
|
-
export const sharedKernel = defineBuilderConfig({
|
|
500
|
-
name: 'SharedKernel',
|
|
501
75
|
injections: [
|
|
502
76
|
{ token: useInterface<ILogger>(), provider: ConsoleLogger },
|
|
503
|
-
{ token: useInterface<IEventBus>(), provider: InMemoryEventBus }
|
|
504
|
-
]
|
|
505
|
-
});
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
```typescript
|
|
509
|
-
// user-module/container.ts
|
|
510
|
-
import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
|
|
511
|
-
import { sharedKernel, ILogger, IEventBus } from '../shared-kernel/container';
|
|
512
|
-
|
|
513
|
-
// User module services
|
|
514
|
-
class UserRepository {
|
|
515
|
-
findById(id: string) { return { id, name: 'John' }; }
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
class UserService {
|
|
519
|
-
constructor(
|
|
520
|
-
private logger: ILogger, // From SharedKernel!
|
|
521
|
-
private eventBus: IEventBus, // From SharedKernel!
|
|
522
|
-
private repo: UserRepository // Local to this module
|
|
523
|
-
) {}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
export const userModule = defineBuilderConfig({
|
|
527
|
-
name: 'UserModule',
|
|
528
|
-
useContainer: sharedKernel, // 👈 Inherit from SharedKernel
|
|
529
|
-
injections: [
|
|
530
|
-
{ token: UserRepository },
|
|
531
77
|
{ token: UserService }
|
|
532
78
|
]
|
|
533
79
|
});
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
```typescript
|
|
537
|
-
// order-module/container.ts
|
|
538
|
-
import { defineBuilderConfig, useInterface } from '@djodjonx/neo-syringe';
|
|
539
|
-
import { sharedKernel, ILogger } from '../shared-kernel/container';
|
|
540
|
-
|
|
541
|
-
class OrderService {
|
|
542
|
-
constructor(private logger: ILogger) {} // From SharedKernel!
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
export const orderModule = defineBuilderConfig({
|
|
546
|
-
name: 'OrderModule',
|
|
547
|
-
useContainer: sharedKernel, // 👈 Same SharedKernel, different module
|
|
548
|
-
injections: [
|
|
549
|
-
{ token: OrderService }
|
|
550
|
-
]
|
|
551
|
-
});
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
```typescript
|
|
555
|
-
// main.ts
|
|
556
|
-
import { userModule } from './user-module/container';
|
|
557
|
-
import { orderModule } from './order-module/container';
|
|
558
|
-
|
|
559
|
-
// Both modules share the same ILogger and IEventBus instances!
|
|
560
|
-
const userService = userModule.resolve(UserService);
|
|
561
|
-
const orderService = orderModule.resolve(OrderService);
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
#### Multi-Level Hierarchy
|
|
565
|
-
|
|
566
|
-
You can chain containers for complex architectures:
|
|
567
80
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const infrastructure = defineBuilderConfig({
|
|
571
|
-
name: 'Infrastructure',
|
|
572
|
-
injections: [
|
|
573
|
-
{ token: useInterface<ILogger>(), provider: ConsoleLogger },
|
|
574
|
-
{ token: useInterface<IDatabase>(), provider: PostgresDatabase }
|
|
575
|
-
]
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
// Level 2: Domain (inherits Infrastructure)
|
|
579
|
-
const domain = defineBuilderConfig({
|
|
580
|
-
name: 'Domain',
|
|
581
|
-
useContainer: infrastructure,
|
|
582
|
-
injections: [
|
|
583
|
-
{ token: UserRepository },
|
|
584
|
-
{ token: OrderRepository }
|
|
585
|
-
]
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
// Level 3: Application (inherits Domain + Infrastructure)
|
|
589
|
-
const application = defineBuilderConfig({
|
|
590
|
-
name: 'Application',
|
|
591
|
-
useContainer: domain, // Gets Domain AND Infrastructure!
|
|
592
|
-
injections: [
|
|
593
|
-
{ token: UserService },
|
|
594
|
-
{ token: OrderService }
|
|
595
|
-
]
|
|
596
|
-
});
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
---
|
|
600
|
-
|
|
601
|
-
### Legacy Migration (`useContainer`)
|
|
602
|
-
|
|
603
|
-
The same `useContainer` mechanism works with legacy DI containers (tsyringe, InversifyJS, Awilix). Neo-Syringe can delegate resolution to any container that has a `resolve()` method.
|
|
604
|
-
|
|
605
|
-
#### Example with tsyringe
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
// legacy-container.ts (existing tsyringe setup)
|
|
609
|
-
import 'reflect-metadata';
|
|
610
|
-
import { container, injectable } from 'tsyringe';
|
|
611
|
-
|
|
612
|
-
@injectable()
|
|
613
|
-
export class AuthService {
|
|
614
|
-
validateToken(token: string) { return true; }
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
@injectable()
|
|
618
|
-
export class LegacyUserRepository {
|
|
619
|
-
findById(id: string) { return { id, name: 'John' }; }
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Register in tsyringe
|
|
623
|
-
container.registerSingleton(AuthService);
|
|
624
|
-
container.registerSingleton(LegacyUserRepository);
|
|
625
|
-
|
|
626
|
-
export { container as legacyContainer };
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
// container.ts (neo-syringe bridging to tsyringe)
|
|
631
|
-
import { defineBuilderConfig, declareContainerTokens, useInterface } from '@djodjonx/neo-syringe';
|
|
632
|
-
import { legacyContainer, AuthService, LegacyUserRepository } from './legacy-container';
|
|
633
|
-
|
|
634
|
-
// Declare what the legacy container provides (for type-safety)
|
|
635
|
-
const legacy = declareContainerTokens<{
|
|
636
|
-
AuthService: AuthService;
|
|
637
|
-
LegacyUserRepository: LegacyUserRepository;
|
|
638
|
-
}>(legacyContainer);
|
|
639
|
-
|
|
640
|
-
// New services using Neo-Syringe
|
|
641
|
-
interface ILogger { log(msg: string): void; }
|
|
642
|
-
class ConsoleLogger implements ILogger {
|
|
643
|
-
log(msg: string) { console.log(msg); }
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
class UserService {
|
|
647
|
-
constructor(
|
|
648
|
-
private auth: AuthService, // From legacy container!
|
|
649
|
-
private repo: LegacyUserRepository, // From legacy container!
|
|
650
|
-
private logger: ILogger // From neo-syringe
|
|
651
|
-
) {}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
export const appContainer = defineBuilderConfig({
|
|
655
|
-
name: 'AppContainer',
|
|
656
|
-
useContainer: legacy, // 👈 Bridge to legacy container
|
|
657
|
-
injections: [
|
|
658
|
-
{ token: useInterface<ILogger>(), provider: ConsoleLogger },
|
|
659
|
-
{ token: UserService } // Dependencies resolved from both containers!
|
|
660
|
-
]
|
|
661
|
-
});
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
```typescript
|
|
665
|
-
// main.ts
|
|
666
|
-
import { appContainer } from './container';
|
|
667
|
-
|
|
668
|
-
const userService = appContainer.resolve(UserService);
|
|
669
|
-
// ✅ AuthService and LegacyUserRepository come from tsyringe
|
|
670
|
-
// ✅ ILogger comes from neo-syringe
|
|
81
|
+
// Use it
|
|
82
|
+
const userService = container.resolve(UserService);
|
|
671
83
|
```
|
|
672
84
|
|
|
673
|
-
|
|
85
|
+
At build time, this generates optimized factory functions. **Zero DI library shipped to production!**
|
|
674
86
|
|
|
675
|
-
|
|
676
|
-
// legacy-inversify.ts
|
|
677
|
-
import 'reflect-metadata';
|
|
678
|
-
import { Container, injectable } from 'inversify';
|
|
87
|
+
## 📖 Documentation
|
|
679
88
|
|
|
680
|
-
|
|
681
|
-
class DatabaseConnection {
|
|
682
|
-
query(sql: string) { return []; }
|
|
683
|
-
}
|
|
89
|
+
For complete documentation, visit **[djodjonx.github.io/neo-syringe](https://djodjonx.github.io/neo-syringe/)**
|
|
684
90
|
|
|
685
|
-
|
|
686
|
-
|
|
91
|
+
| Topic | Description |
|
|
92
|
+
|-------|-------------|
|
|
93
|
+
| [Getting Started](https://djodjonx.github.io/neo-syringe/guide/getting-started) | Installation and first container |
|
|
94
|
+
| [Why Neo-Syringe?](https://djodjonx.github.io/neo-syringe/guide/why-neo-syringe) | Comparison with traditional DI |
|
|
95
|
+
| [Injection Types](https://djodjonx.github.io/neo-syringe/guide/injection-types) | Classes, interfaces, factories, primitives |
|
|
96
|
+
| [Lifecycle](https://djodjonx.github.io/neo-syringe/guide/lifecycle) | Singleton vs transient |
|
|
97
|
+
| [Scoped Injections](https://djodjonx.github.io/neo-syringe/guide/scoped-injections) | Override parent container tokens |
|
|
98
|
+
| [Parent Container](https://djodjonx.github.io/neo-syringe/guide/parent-container) | SharedKernel architecture |
|
|
99
|
+
| [Legacy Migration](https://djodjonx.github.io/neo-syringe/guide/legacy-migration) | Bridge tsyringe, InversifyJS |
|
|
100
|
+
| [Generated Code](https://djodjonx.github.io/neo-syringe/guide/generated-code) | What the compiler produces |
|
|
101
|
+
| [CLI Validator](https://djodjonx.github.io/neo-syringe/guide/cli) | Validate in CI/CD |
|
|
102
|
+
| [IDE Plugin](https://djodjonx.github.io/neo-syringe/guide/ide-plugin) | Real-time error detection |
|
|
103
|
+
| [API Reference](https://djodjonx.github.io/neo-syringe/api/types) | Types and functions |
|
|
104
|
+
|
|
105
|
+
## 🔧 Build Plugin Setup
|
|
687
106
|
|
|
688
|
-
|
|
689
|
-
|
|
107
|
+
<details>
|
|
108
|
+
<summary><strong>Vite</strong></summary>
|
|
690
109
|
|
|
691
110
|
```typescript
|
|
692
|
-
|
|
693
|
-
import { defineBuilderConfig, declareContainerTokens } from '@djodjonx/neo-syringe';
|
|
694
|
-
import { inversifyContainer, DatabaseConnection } from './legacy-inversify';
|
|
695
|
-
|
|
696
|
-
const legacy = declareContainerTokens<{
|
|
697
|
-
DatabaseConnection: DatabaseConnection;
|
|
698
|
-
}>(inversifyContainer);
|
|
699
|
-
|
|
700
|
-
class ReportService {
|
|
701
|
-
constructor(private db: DatabaseConnection) {} // From Inversify!
|
|
702
|
-
}
|
|
111
|
+
import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
|
|
703
112
|
|
|
704
|
-
export
|
|
705
|
-
|
|
706
|
-
injections: [
|
|
707
|
-
{ token: ReportService }
|
|
708
|
-
]
|
|
113
|
+
export default defineConfig({
|
|
114
|
+
plugins: [neoSyringePlugin.vite()]
|
|
709
115
|
});
|
|
710
116
|
```
|
|
117
|
+
</details>
|
|
711
118
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
When `container.resolve(Token)` is called:
|
|
715
|
-
|
|
716
|
-
```
|
|
717
|
-
1. Neo-Syringe checks its own registrations
|
|
718
|
-
└── Found? → Return instance
|
|
719
|
-
└── Not found? ↓
|
|
720
|
-
|
|
721
|
-
2. Delegate to parent container (useContainer)
|
|
722
|
-
└── Found? → Return instance
|
|
723
|
-
└── Not found? ↓
|
|
724
|
-
|
|
725
|
-
3. Throw "Service not found" error
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
> 💡 **Migration Strategy**: Start by bridging your entire legacy container, then gradually move services to Neo-Syringe. Once all services are migrated, remove `useContainer`.
|
|
729
|
-
|
|
730
|
-
#### How Legacy Bridging Works Internally
|
|
731
|
-
|
|
732
|
-
**At Compile-Time:**
|
|
733
|
-
|
|
734
|
-
1. `declareContainerTokens<T>()` is analyzed
|
|
735
|
-
2. The type `T` properties are extracted (e.g., `{ AuthService, UserRepo }`)
|
|
736
|
-
3. These tokens are added to `parentProvidedTokens`
|
|
737
|
-
4. The GraphValidator accepts these as valid dependencies
|
|
738
|
-
5. The Generator outputs: `new NeoContainer(undefined, [legacyContainer], 'MyApp')`
|
|
739
|
-
|
|
740
|
-
**At Runtime:**
|
|
741
|
-
|
|
742
|
-
```typescript
|
|
743
|
-
// Generated code (simplified)
|
|
744
|
-
class NeoContainer {
|
|
745
|
-
constructor(
|
|
746
|
-
private parent?: any,
|
|
747
|
-
private legacy?: any[], // ← Your tsyringe/inversify container
|
|
748
|
-
private name?: string
|
|
749
|
-
) {}
|
|
750
|
-
|
|
751
|
-
resolve(token: any): any {
|
|
752
|
-
// 1. Try local resolution
|
|
753
|
-
const local = this.resolveLocal(token);
|
|
754
|
-
if (local !== undefined) return local;
|
|
755
|
-
|
|
756
|
-
// 2. Delegate to legacy containers
|
|
757
|
-
if (this.legacy) {
|
|
758
|
-
for (const container of this.legacy) {
|
|
759
|
-
try {
|
|
760
|
-
return container.resolve(token); // ← Calls tsyringe.resolve()!
|
|
761
|
-
} catch (e) { /* try next */ }
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
throw new Error(`Service not found: ${token}`);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
**Validation Features:**
|
|
771
|
-
|
|
772
|
-
| Check | Description |
|
|
773
|
-
|-------|-------------|
|
|
774
|
-
| ✅ Missing binding | Error if dependency not in local OR legacy container |
|
|
775
|
-
| ✅ Duplicate detection | Error if token already registered in parent/legacy |
|
|
776
|
-
| ✅ Type safety | `declareContainerTokens<T>()` provides TypeScript types |
|
|
777
|
-
|
|
778
|
-
### Partials (Modular Config)
|
|
779
|
-
|
|
780
|
-
Split configuration into reusable blocks.
|
|
119
|
+
<details>
|
|
120
|
+
<summary><strong>Rollup</strong></summary>
|
|
781
121
|
|
|
782
122
|
```typescript
|
|
783
|
-
|
|
784
|
-
export const loggingConfig = definePartialConfig({
|
|
785
|
-
injections: [
|
|
786
|
-
{ token: useInterface<ILogger>(), provider: ConsoleLogger }
|
|
787
|
-
]
|
|
788
|
-
});
|
|
123
|
+
import { neoSyringePlugin } from '@djodjonx/neo-syringe/plugin';
|
|
789
124
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
injections: [
|
|
794
|
-
{ token: UserService }
|
|
795
|
-
]
|
|
796
|
-
});
|
|
125
|
+
export default {
|
|
126
|
+
plugins: [neoSyringePlugin.rollup()]
|
|
127
|
+
};
|
|
797
128
|
```
|
|
129
|
+
</details>
|
|
798
130
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
Validate your dependency graph in CI/CD pipelines.
|
|
802
|
-
|
|
803
|
-
```bash
|
|
804
|
-
npx neo-syringe
|
|
805
|
-
```
|
|
131
|
+
<details>
|
|
132
|
+
<summary><strong>Webpack</strong></summary>
|
|
806
133
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
Found 45 services.
|
|
812
|
-
🛡️ Validating graph...
|
|
813
|
-
✅ Validation passed! No circular dependencies or missing bindings found.
|
|
134
|
+
```javascript
|
|
135
|
+
module.exports = {
|
|
136
|
+
plugins: [require('@djodjonx/neo-syringe/plugin').webpack()]
|
|
137
|
+
};
|
|
814
138
|
```
|
|
139
|
+
</details>
|
|
815
140
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
## IDE Plugin for Real-Time Validation
|
|
819
|
-
|
|
820
|
-
Get immediate feedback on configuration errors.
|
|
141
|
+
## 🛡️ IDE Support
|
|
821
142
|
|
|
822
|
-
|
|
143
|
+
Add to `tsconfig.json` for real-time error detection:
|
|
823
144
|
|
|
824
145
|
```json
|
|
825
146
|
{
|
|
@@ -831,35 +152,7 @@ Get immediate feedback on configuration errors.
|
|
|
831
152
|
}
|
|
832
153
|
```
|
|
833
154
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
In VS Code: `Ctrl+Shift+P` → "TypeScript: Select TypeScript Version" → "Use Workspace Version"
|
|
837
|
-
|
|
838
|
-
### Detected Errors
|
|
839
|
-
|
|
840
|
-
| Error | Message |
|
|
841
|
-
| :--- | :--- |
|
|
842
|
-
| 🔴 **Circular Dependency** | `[Neo-Syringe] Circular dependency detected: A -> B -> A` |
|
|
843
|
-
| 🔴 **Missing Binding** | `[Neo-Syringe] Missing binding: 'Service' depends on 'I', but no provider registered.` |
|
|
844
|
-
| 🔴 **Duplicate** | `[Neo-Syringe] Duplicate registration: 'Service' is already registered.` |
|
|
845
|
-
|
|
846
|
-
---
|
|
847
|
-
|
|
848
|
-
## Troubleshooting
|
|
849
|
-
|
|
850
|
-
### The plugin doesn't detect errors
|
|
851
|
-
|
|
852
|
-
1. Verify TypeScript uses the workspace version
|
|
853
|
-
2. Restart the TypeScript server (`Cmd+Shift+P` → "TypeScript: Restart TS Server")
|
|
854
|
-
3. Check that `@djodjonx/neo-syringe/lsp` is in your `tsconfig.json` plugins
|
|
855
|
-
|
|
856
|
-
### Build plugin not working
|
|
857
|
-
|
|
858
|
-
Ensure `unplugin` is installed and the plugin is correctly configured in your bundler.
|
|
859
|
-
|
|
860
|
-
---
|
|
861
|
-
|
|
862
|
-
## License
|
|
155
|
+
## 📄 License
|
|
863
156
|
|
|
864
157
|
MIT
|
|
865
158
|
|