@nextrush/di 3.0.0
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/LICENSE +21 -0
- package/README.md +481 -0
- package/dist/index.d.ts +517 -0
- package/dist/index.js +449 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanzim (NextRush)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# @nextrush/di
|
|
2
|
+
|
|
3
|
+
Lightweight dependency injection container for NextRush v3.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Constructor Injection** — Automatic dependency resolution via TypeScript metadata
|
|
8
|
+
- **Singleton & Transient Scopes** — Control instance lifecycle per service
|
|
9
|
+
- **Circular Dependency Detection** — O(1) Set-based detection with cycle visualization, catches tsyringe-internal chains
|
|
10
|
+
- **Production-Grade Errors** — Actionable messages with fix suggestions (`@Service()`, `@Repository()`, `@Config()` hints)
|
|
11
|
+
- **Optional Dependencies** — `@Optional()` decorator for graceful handling of missing services
|
|
12
|
+
- **Test-Friendly** — Isolated containers and instance clearing for test setup
|
|
13
|
+
|
|
14
|
+
## Development
|
|
15
|
+
|
|
16
|
+
For the best development experience with full decorator and DI support, use **`@nextrush/dev`**.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add -D @nextrush/dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then in your `package.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "nextrush dev"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Why?
|
|
33
|
+
|
|
34
|
+
TypeScript's `emitDecoratorMetadata` option emits runtime type information that allows the DI container to automatically resolve constructor dependencies. Most modern fast runners (`tsx`, `esbuild`, `node --experimental-strip-types`) strip types but **do not** emit this metadata, causing errors like:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
TypeInfo not known for "UserService"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Runtime | Decorator Metadata | Recommended |
|
|
41
|
+
| ----------------- | ------------------ | ---------------- |
|
|
42
|
+
| **nextrush dev** | Full Support | Yes |
|
|
43
|
+
| **tsc + node** | Full Support | Yes (Production) |
|
|
44
|
+
| **tsx / esbuild** | Not Supported | No |
|
|
45
|
+
| **ts-node --esm** | Issues | No |
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add @nextrush/di
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> If you use the `nextrush` meta-package, `reflect-metadata` is auto-imported. Otherwise, install it separately: `pnpm add reflect-metadata` and add `import 'reflect-metadata'` at your entry point.
|
|
54
|
+
|
|
55
|
+
## TypeScript Configuration
|
|
56
|
+
|
|
57
|
+
**Required** `tsconfig.json` settings:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"compilerOptions": {
|
|
62
|
+
"experimentalDecorators": true,
|
|
63
|
+
"emitDecoratorMetadata": true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import 'reflect-metadata'; // Required when NOT using the nextrush meta-package
|
|
72
|
+
import { Service, Repository, container } from '@nextrush/di';
|
|
73
|
+
|
|
74
|
+
@Repository()
|
|
75
|
+
class UserRepository {
|
|
76
|
+
findAll() {
|
|
77
|
+
return [{ id: 1, name: 'Alice' }];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Service()
|
|
82
|
+
class UserService {
|
|
83
|
+
constructor(private repo: UserRepository) {}
|
|
84
|
+
|
|
85
|
+
getUsers() {
|
|
86
|
+
return this.repo.findAll();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const userService = container.resolve(UserService);
|
|
91
|
+
console.log(userService.getUsers()); // [{ id: 1, name: 'Alice' }]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### Decorators
|
|
97
|
+
|
|
98
|
+
#### `@Service(options?)`
|
|
99
|
+
|
|
100
|
+
Mark a class as an injectable service. Singletons by default.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
@Service()
|
|
104
|
+
class MyService {}
|
|
105
|
+
|
|
106
|
+
// Transient (new instance each time)
|
|
107
|
+
@Service({ scope: 'transient' })
|
|
108
|
+
class RequestLogger {}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**`ServiceOptions`:**
|
|
112
|
+
|
|
113
|
+
| Property | Type | Default | Description |
|
|
114
|
+
| -------- | ---------------------------- | ------------- | ------------------ |
|
|
115
|
+
| `scope` | `'singleton' \| 'transient'` | `'singleton'` | Instance lifecycle |
|
|
116
|
+
|
|
117
|
+
#### `@Repository(options?)`
|
|
118
|
+
|
|
119
|
+
Semantic alias for `@Service()`. Sets metadata type to `'repository'` instead of `'service'`. Use for data access layers.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
@Repository()
|
|
123
|
+
class UserRepository {
|
|
124
|
+
findById(id: string) {
|
|
125
|
+
return db.users.find(id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### `@inject(token)`
|
|
131
|
+
|
|
132
|
+
Explicitly inject a dependency by token. Use for interfaces, string tokens, or symbol tokens.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const DATABASE_TOKEN = Symbol('Database');
|
|
136
|
+
|
|
137
|
+
@Service()
|
|
138
|
+
class UserService {
|
|
139
|
+
constructor(
|
|
140
|
+
@inject(DATABASE_TOKEN) private db: IDatabase,
|
|
141
|
+
@inject('API_KEY') private apiKey: string
|
|
142
|
+
) {}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### `@AutoInjectable()`
|
|
147
|
+
|
|
148
|
+
Mark a class as injectable in the container. Sets service type metadata to `'service'`.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
@AutoInjectable()
|
|
152
|
+
class FeatureService {
|
|
153
|
+
constructor(private logger: Logger) {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Resolve through the container
|
|
157
|
+
const service = container.resolve(FeatureService);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
> **Note:** Despite the name, `@AutoInjectable()` does not enable dependency injection via `new`. The current implementation uses tsyringe's `injectable()` decorator internally. Dependencies are resolved only through `container.resolve()`.
|
|
161
|
+
|
|
162
|
+
#### `delay(tokenFactory)`
|
|
163
|
+
|
|
164
|
+
Defer resolution to break circular dependencies. Returns a lazy token for use with `@inject()`.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { delay, inject, Service } from '@nextrush/di';
|
|
168
|
+
|
|
169
|
+
@Service()
|
|
170
|
+
class ServiceA {
|
|
171
|
+
constructor(@inject(delay(() => ServiceB)) private b: ServiceB) {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@Service()
|
|
175
|
+
class ServiceB {
|
|
176
|
+
constructor(@inject(delay(() => ServiceA)) private a: ServiceA) {}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### `@Optional()`
|
|
181
|
+
|
|
182
|
+
Mark a constructor parameter as optional. When the dependency is not registered, the container injects `undefined` instead of throwing.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { Service, Optional, inject } from '@nextrush/di';
|
|
186
|
+
|
|
187
|
+
@Service()
|
|
188
|
+
class NotificationService {
|
|
189
|
+
constructor(
|
|
190
|
+
@Optional() private emailService?: EmailService,
|
|
191
|
+
@inject('SLACK_TOKEN') @Optional() private slackToken?: string
|
|
192
|
+
) {}
|
|
193
|
+
|
|
194
|
+
notify(message: string) {
|
|
195
|
+
if (this.emailService) {
|
|
196
|
+
this.emailService.send(message);
|
|
197
|
+
}
|
|
198
|
+
if (this.slackToken) {
|
|
199
|
+
// send to Slack
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### `isParameterOptional(target, parameterIndex)`
|
|
206
|
+
|
|
207
|
+
Check if a specific constructor parameter is marked as optional.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { isParameterOptional, Optional, Service } from '@nextrush/di';
|
|
211
|
+
|
|
212
|
+
@Service()
|
|
213
|
+
class MyService {
|
|
214
|
+
constructor(@Optional() private dep?: SomeDep) {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
isParameterOptional(MyService, 0); // true
|
|
218
|
+
isParameterOptional(MyService, 1); // false
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `getOptionalParams(target)`
|
|
222
|
+
|
|
223
|
+
Get all optional parameter indices for a class. Returns a `ReadonlySet<number>`.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { getOptionalParams } from '@nextrush/di';
|
|
227
|
+
|
|
228
|
+
const optionals = getOptionalParams(MyService);
|
|
229
|
+
// Set { 0 }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Utility Functions
|
|
233
|
+
|
|
234
|
+
#### `hasServiceMetadata(target)`
|
|
235
|
+
|
|
236
|
+
Check if a class has DI metadata (decorated with `@Service()` or `@Repository()`).
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { hasServiceMetadata, Service } from '@nextrush/di';
|
|
240
|
+
|
|
241
|
+
@Service()
|
|
242
|
+
class MyService {}
|
|
243
|
+
|
|
244
|
+
hasServiceMetadata(MyService); // true
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `getServiceType(target)`
|
|
248
|
+
|
|
249
|
+
Get the service type from a decorated class. Returns `'service'`, `'repository'`, or `undefined`.
|
|
250
|
+
|
|
251
|
+
#### `getServiceScope(target)`
|
|
252
|
+
|
|
253
|
+
Get the scope from a decorated class. Returns `'singleton'`, `'transient'`, or `undefined`.
|
|
254
|
+
|
|
255
|
+
### Container
|
|
256
|
+
|
|
257
|
+
#### `container.register(token, provider)`
|
|
258
|
+
|
|
259
|
+
Register a dependency with the container.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// Class provider
|
|
263
|
+
container.register(UserService, { useClass: UserService });
|
|
264
|
+
|
|
265
|
+
// Value provider
|
|
266
|
+
container.register('CONFIG', { useValue: { port: 3000 } });
|
|
267
|
+
|
|
268
|
+
// Factory provider — receives the container for nested resolution
|
|
269
|
+
container.register(Logger, {
|
|
270
|
+
useFactory: (c) => new Logger(c.resolve('CONFIG')),
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### `container.resolve(token)`
|
|
275
|
+
|
|
276
|
+
Resolve a dependency from the container.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
const service = container.resolve(UserService);
|
|
280
|
+
const config = container.resolve<Config>('CONFIG');
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### `container.resolveAll(token)`
|
|
284
|
+
|
|
285
|
+
Resolve all dependencies registered under a token. Returns an empty array if none are registered.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
container.register('Plugin', { useValue: pluginA });
|
|
289
|
+
container.register('Plugin', { useValue: pluginB });
|
|
290
|
+
|
|
291
|
+
const plugins = container.resolveAll<Plugin>('Plugin');
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `container.isRegistered(token)`
|
|
295
|
+
|
|
296
|
+
Check if a token is registered.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
if (container.isRegistered(UserService)) {
|
|
300
|
+
// ...
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### `container.clearInstances()`
|
|
305
|
+
|
|
306
|
+
Clear cached singleton instances. Registrations remain — the next `resolve()` creates fresh instances.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
beforeEach(() => {
|
|
310
|
+
container.clearInstances();
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### `container.reset()`
|
|
315
|
+
|
|
316
|
+
Reset the container completely, removing all registrations and instances.
|
|
317
|
+
|
|
318
|
+
#### `container.createChild()`
|
|
319
|
+
|
|
320
|
+
Create a child container. The child inherits parent registrations but can override them independently.
|
|
321
|
+
|
|
322
|
+
#### `createContainer()`
|
|
323
|
+
|
|
324
|
+
Create a new isolated container with no inherited registrations.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
const testContainer = createContainer();
|
|
328
|
+
testContainer.register(UserService, { useClass: MockUserService });
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Error Handling
|
|
332
|
+
|
|
333
|
+
All errors extend `DIError` and include actionable messages:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import {
|
|
337
|
+
DIError,
|
|
338
|
+
DependencyResolutionError,
|
|
339
|
+
CircularDependencyError,
|
|
340
|
+
TypeInferenceError,
|
|
341
|
+
MissingDependencyError,
|
|
342
|
+
InvalidProviderError,
|
|
343
|
+
ContainerDisposedError,
|
|
344
|
+
} from '@nextrush/di';
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
container.resolve(UnregisteredService);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error instanceof DependencyResolutionError) {
|
|
350
|
+
console.log(error.missingDependency); // token name
|
|
351
|
+
console.log(error.chain); // resolution path
|
|
352
|
+
}
|
|
353
|
+
if (error instanceof CircularDependencyError) {
|
|
354
|
+
console.log(error.cycle); // ['ServiceA', 'ServiceB', ...]
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
| Error | Cause |
|
|
360
|
+
| --------------------------- | --------------------------------------------------------------------------------------------- |
|
|
361
|
+
| `DependencyResolutionError` | Token not registered — includes fix suggestions (`@Service()`, import order, manual register) |
|
|
362
|
+
| `CircularDependencyError` | Circular dependency detected (wrapper-level + tsyringe-internal chains) |
|
|
363
|
+
| `MissingDependencyError` | _(Deprecated)_ Use `DependencyResolutionError` instead |
|
|
364
|
+
| `InvalidProviderError` | Provider missing `useClass`, `useValue`, or `useFactory` |
|
|
365
|
+
| `TypeInferenceError` | Constructor parameter type not available at runtime |
|
|
366
|
+
| `ContainerDisposedError` | Container has been reset or disposed |
|
|
367
|
+
|
|
368
|
+
## TypeScript Exports
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// Container
|
|
372
|
+
import { container, createContainer } from '@nextrush/di';
|
|
373
|
+
|
|
374
|
+
// Decorators
|
|
375
|
+
import { Service, Repository, AutoInjectable, Optional, inject, delay } from '@nextrush/di';
|
|
376
|
+
|
|
377
|
+
// Utility functions
|
|
378
|
+
import {
|
|
379
|
+
hasServiceMetadata,
|
|
380
|
+
getServiceType,
|
|
381
|
+
getServiceScope,
|
|
382
|
+
isParameterOptional,
|
|
383
|
+
getOptionalParams,
|
|
384
|
+
} from '@nextrush/di';
|
|
385
|
+
|
|
386
|
+
// Metadata keys
|
|
387
|
+
import { METADATA_KEYS } from '@nextrush/di';
|
|
388
|
+
|
|
389
|
+
// Error classes
|
|
390
|
+
import {
|
|
391
|
+
DIError,
|
|
392
|
+
DependencyResolutionError,
|
|
393
|
+
CircularDependencyError,
|
|
394
|
+
MissingDependencyError,
|
|
395
|
+
InvalidProviderError,
|
|
396
|
+
TypeInferenceError,
|
|
397
|
+
ContainerDisposedError,
|
|
398
|
+
} from '@nextrush/di';
|
|
399
|
+
|
|
400
|
+
// Types
|
|
401
|
+
import type {
|
|
402
|
+
ContainerInterface,
|
|
403
|
+
Provider,
|
|
404
|
+
ClassProvider,
|
|
405
|
+
ValueProvider,
|
|
406
|
+
FactoryProvider,
|
|
407
|
+
Token,
|
|
408
|
+
Constructor,
|
|
409
|
+
Scope,
|
|
410
|
+
ServiceOptions,
|
|
411
|
+
} from '@nextrush/di';
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Integration with Guards
|
|
415
|
+
|
|
416
|
+
Class-based guards implementing `CanActivate` are resolved from the DI container:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import { Service } from '@nextrush/di';
|
|
420
|
+
import type { CanActivate, GuardContext } from '@nextrush/decorators';
|
|
421
|
+
|
|
422
|
+
@Service()
|
|
423
|
+
class AuthGuard implements CanActivate {
|
|
424
|
+
constructor(private authService: AuthService) {}
|
|
425
|
+
|
|
426
|
+
async canActivate(ctx: GuardContext): Promise<boolean> {
|
|
427
|
+
const token = ctx.get('authorization');
|
|
428
|
+
if (!token) return false;
|
|
429
|
+
|
|
430
|
+
const user = await this.authService.verify(token);
|
|
431
|
+
ctx.state.user = user;
|
|
432
|
+
return Boolean(user);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
The `@nextrush/controllers` plugin automatically detects class guards and resolves them from the container.
|
|
438
|
+
|
|
439
|
+
## Troubleshooting
|
|
440
|
+
|
|
441
|
+
### Error: "TypeInfo not known for X"
|
|
442
|
+
|
|
443
|
+
**Cause**: `emitDecoratorMetadata` is not being emitted at runtime.
|
|
444
|
+
|
|
445
|
+
**Fix**: Use `@nextrush/dev` for development. It automatically handles metadata emission.
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
# ❌ Doesn't work (no decorator metadata)
|
|
449
|
+
npx tsx src/index.ts
|
|
450
|
+
|
|
451
|
+
# ✅ Works (full decorator support)
|
|
452
|
+
npx nextrush dev
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Error: "reflect-metadata not found"
|
|
456
|
+
|
|
457
|
+
**Cause**: `reflect-metadata` must be imported before decorators.
|
|
458
|
+
|
|
459
|
+
**Fix**: Import it first in your entry point:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import 'reflect-metadata'; // MUST be first!
|
|
463
|
+
import { Service } from '@nextrush/di';
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Constructor parameters not injected
|
|
467
|
+
|
|
468
|
+
**Cause**: Class is missing `@Service()` decorator.
|
|
469
|
+
|
|
470
|
+
**Fix**: Add the decorator:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
@Service() // Required for DI!
|
|
474
|
+
class MyService {
|
|
475
|
+
constructor(private dep: SomeDependency) {}
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## License
|
|
480
|
+
|
|
481
|
+
MIT
|