navi-di 0.1.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/CODE_OF_CONDUCT.md +20 -0
- package/CONTRIBUTING.md +28 -0
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/SECURITY.md +23 -0
- package/dist/container/container.d.ts +14 -0
- package/dist/container/container.js +108 -0
- package/dist/container/index.d.ts +1 -0
- package/dist/container/index.js +1 -0
- package/dist/container/registry.d.ts +11 -0
- package/dist/container/registry.js +37 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +2 -0
- package/dist/decorators/inject.d.ts +2 -0
- package/dist/decorators/inject.js +10 -0
- package/dist/decorators/service.d.ts +3 -0
- package/dist/decorators/service.js +15 -0
- package/dist/errors/circular-dependency-error.d.ts +5 -0
- package/dist/errors/circular-dependency-error.js +6 -0
- package/dist/errors/container-duplicated-error.d.ts +5 -0
- package/dist/errors/container-duplicated-error.js +6 -0
- package/dist/errors/default-container-id-error.d.ts +4 -0
- package/dist/errors/default-container-id-error.js +6 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.js +4 -0
- package/dist/errors/service-not-found-error.d.ts +5 -0
- package/dist/errors/service-not-found-error.js +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/types/constructable.d.ts +2 -0
- package/dist/types/constructable.js +0 -0
- package/dist/types/container.d.ts +13 -0
- package/dist/types/container.js +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +2 -0
- package/dist/types/injection.d.ts +6 -0
- package/dist/types/injection.js +1 -0
- package/dist/types/service.d.ts +7 -0
- package/dist/types/service.js +0 -0
- package/package.json +70 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our standard
|
|
4
|
+
|
|
5
|
+
Please be respectful, specific, and constructive.
|
|
6
|
+
|
|
7
|
+
We expect contributors to:
|
|
8
|
+
|
|
9
|
+
- discuss technical disagreements in good faith;
|
|
10
|
+
- focus feedback on code, design, and behavior rather than people;
|
|
11
|
+
- avoid harassment, abuse, discrimination, and personal attacks; and
|
|
12
|
+
- help keep the project welcoming to new contributors.
|
|
13
|
+
|
|
14
|
+
## Enforcement
|
|
15
|
+
|
|
16
|
+
Project maintainers may edit, hide, or remove content that violates this code of conduct and may restrict future participation when needed to protect the project.
|
|
17
|
+
|
|
18
|
+
## Reporting
|
|
19
|
+
|
|
20
|
+
Please report serious or repeated issues through the security contact listed in [SECURITY.md](./SECURITY.md).
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for contributing to `navi-di`.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This repository is focused on a small, explicit DI core for standard ECMAScript decorators. Please keep proposals aligned with that goal and avoid bundling unrelated framework abstractions into the base package.
|
|
8
|
+
|
|
9
|
+
## Before you open a pull request
|
|
10
|
+
|
|
11
|
+
1. Open or reference an issue when the change affects public API or package behavior.
|
|
12
|
+
2. Keep changes narrowly scoped and explain the motivation.
|
|
13
|
+
3. Update docs when user-facing behavior changes.
|
|
14
|
+
|
|
15
|
+
## Git hooks
|
|
16
|
+
|
|
17
|
+
This repository uses Lefthook.
|
|
18
|
+
|
|
19
|
+
- `pre-commit` runs fast staged-file checks for lint and formatting.
|
|
20
|
+
|
|
21
|
+
If hooks stop working locally, reinstall them with `bun run hooks:install`.
|
|
22
|
+
|
|
23
|
+
## Pull request checklist
|
|
24
|
+
|
|
25
|
+
- the change is focused and documented;
|
|
26
|
+
- scripts in the local checks section pass;
|
|
27
|
+
- package exports and Node compatibility were kept intact;
|
|
28
|
+
- new behavior includes tests or a clear explanation for why tests are not needed.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Naviary Sanctuary
|
|
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,283 @@
|
|
|
1
|
+
# navi-di
|
|
2
|
+
|
|
3
|
+
`navi-di` is a dependency injection library built for standard ECMAScript decorators.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
`navi-di` can be installed with any common JavaScript package manager:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install navi-di
|
|
11
|
+
pnpm add navi-di
|
|
12
|
+
yarn add navi-di
|
|
13
|
+
bun add navi-di
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The current implementation focuses on a compact core:
|
|
17
|
+
|
|
18
|
+
- `@Service()` registers classes in the default container.
|
|
19
|
+
- `@Inject()` wires decorated class fields from the active container.
|
|
20
|
+
- `Container.of()` resolves services from the default container or from named containers.
|
|
21
|
+
- `singleton`, `container`, and `transient` scopes control instance lifetime.
|
|
22
|
+
- Circular graphs and missing services fail with explicit runtime errors.
|
|
23
|
+
|
|
24
|
+
## What is implemented today
|
|
25
|
+
|
|
26
|
+
The repository is no longer just scaffolding. The source and tests currently cover:
|
|
27
|
+
|
|
28
|
+
- class registration through `@Service()`;
|
|
29
|
+
- field injection through `@Inject()`;
|
|
30
|
+
- named containers via `Container.of(id)`;
|
|
31
|
+
- default-container fallback for named containers;
|
|
32
|
+
- per-container caching for `container` scope;
|
|
33
|
+
- shared instances for `singleton` scope;
|
|
34
|
+
- fresh instances for `transient` scope;
|
|
35
|
+
- cache reset and registration reset through `container.reset()`;
|
|
36
|
+
- error handling for circular dependencies, missing services, and invalid container operations.
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- Runtime target: Node.js `>=18.18`
|
|
41
|
+
- Local development toolchain: Bun `>=1.3.0`
|
|
42
|
+
- Language target: TypeScript 5 style standard decorators
|
|
43
|
+
- Module format: ESM
|
|
44
|
+
|
|
45
|
+
No environment variables or external services are required for local development.
|
|
46
|
+
|
|
47
|
+
## Public entry points
|
|
48
|
+
|
|
49
|
+
The package root currently exports:
|
|
50
|
+
|
|
51
|
+
- `Container`
|
|
52
|
+
- `Service`
|
|
53
|
+
- `Inject`
|
|
54
|
+
- `Constructable` / `AbstractConstructable`
|
|
55
|
+
- `ServiceIdentifier`
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { Container, Inject, Service } from 'navi-di';
|
|
61
|
+
|
|
62
|
+
@Service()
|
|
63
|
+
class LoggerService {
|
|
64
|
+
public log(message: string) {
|
|
65
|
+
console.log(message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Service()
|
|
70
|
+
class HandlerService {
|
|
71
|
+
@Inject(LoggerService)
|
|
72
|
+
public logger!: LoggerService;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const handler = Container.of().get(HandlerService);
|
|
76
|
+
|
|
77
|
+
handler.logger.log('hello from navi-di');
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
How resolution works:
|
|
81
|
+
|
|
82
|
+
1. `@Service()` stores the class metadata in the default container.
|
|
83
|
+
2. `@Inject()` records which decorated fields should be resolved.
|
|
84
|
+
3. `Container.of().get(HandlerService)` creates `HandlerService`.
|
|
85
|
+
4. The container resolves each injected field from the same container instance.
|
|
86
|
+
|
|
87
|
+
## Service lifetimes
|
|
88
|
+
|
|
89
|
+
`navi-di` currently supports three scopes.
|
|
90
|
+
|
|
91
|
+
### `container`
|
|
92
|
+
|
|
93
|
+
The default scope. One instance is cached per container.
|
|
94
|
+
|
|
95
|
+
- repeated `get()` calls in the same container reuse the same instance;
|
|
96
|
+
- named containers receive their own isolated instance;
|
|
97
|
+
- named containers lazily clone container-scoped registrations from the default container on first access.
|
|
98
|
+
|
|
99
|
+
### `singleton`
|
|
100
|
+
|
|
101
|
+
One shared instance across all containers.
|
|
102
|
+
|
|
103
|
+
- singleton registrations are effectively stored in the default container;
|
|
104
|
+
- resolving the same singleton from a named container returns the same shared instance as the default container.
|
|
105
|
+
|
|
106
|
+
### `transient`
|
|
107
|
+
|
|
108
|
+
A new instance is created on every `get()` call.
|
|
109
|
+
|
|
110
|
+
## Named containers
|
|
111
|
+
|
|
112
|
+
Use named containers when you want isolated request, job, or unit-of-work state.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const requestA = Container.of('request-a');
|
|
116
|
+
const requestB = Container.of('request-b');
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Current behavior:
|
|
120
|
+
|
|
121
|
+
- `Container.of()` and `Container.of('default')` return the same default container;
|
|
122
|
+
- `Container.of('name')` reuses the same named container for the same id;
|
|
123
|
+
- named containers fall back to registrations stored in the default container;
|
|
124
|
+
- `container` scope becomes container-local after first resolution in a named container;
|
|
125
|
+
- `singleton` scope stays shared across the whole registry.
|
|
126
|
+
|
|
127
|
+
## Decorators
|
|
128
|
+
|
|
129
|
+
### `@Service(options?)`
|
|
130
|
+
|
|
131
|
+
Registers a class in the default container.
|
|
132
|
+
|
|
133
|
+
Options supported today:
|
|
134
|
+
|
|
135
|
+
- `id?: ServiceIdentifier`
|
|
136
|
+
- `scope?: 'singleton' | 'container' | 'transient'`
|
|
137
|
+
|
|
138
|
+
Example with a custom id:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { Container, Service } from 'navi-di';
|
|
142
|
+
|
|
143
|
+
@Service({ id: 'logger', scope: 'singleton' })
|
|
144
|
+
class LoggerService {}
|
|
145
|
+
|
|
146
|
+
const logger = Container.of().get('logger');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Note: a custom string id works for manual resolution through `container.get(...)`, but `@Inject()` currently accepts a constructable class dependency rather than an arbitrary token.
|
|
150
|
+
|
|
151
|
+
### `@Inject(dependency)`
|
|
152
|
+
|
|
153
|
+
Marks a decorated class field for property injection.
|
|
154
|
+
|
|
155
|
+
Current characteristics:
|
|
156
|
+
|
|
157
|
+
- injection is property-based, not constructor-based;
|
|
158
|
+
- services are instantiated with `new Class()` and therefore must be resolvable without constructor arguments;
|
|
159
|
+
- dependencies are resolved from the active container;
|
|
160
|
+
- multiple decorated fields on the same class are supported;
|
|
161
|
+
- injected fields are defined as writable and configurable own properties on the created instance;
|
|
162
|
+
- injected fields are assigned after construction, so they are not available inside constructors or field initializers.
|
|
163
|
+
|
|
164
|
+
## Container API
|
|
165
|
+
|
|
166
|
+
### `Container.of(id?)`
|
|
167
|
+
|
|
168
|
+
Returns the default container or a named container.
|
|
169
|
+
|
|
170
|
+
### `container.get(id)`
|
|
171
|
+
|
|
172
|
+
Resolves a service by class or service identifier.
|
|
173
|
+
|
|
174
|
+
Throws:
|
|
175
|
+
|
|
176
|
+
- `ServiceNotFoundError` when no registration exists;
|
|
177
|
+
- `CircularDependencyError` when the current resolution path loops back to an in-progress dependency.
|
|
178
|
+
|
|
179
|
+
### `container.has(id)`
|
|
180
|
+
|
|
181
|
+
Checks whether the current container has a local registration.
|
|
182
|
+
|
|
183
|
+
For named containers, this does not report default-container registrations until the service has been materialized locally.
|
|
184
|
+
|
|
185
|
+
In practice, that means `has()` becomes `true` after first resolution for `container`-scoped services, but can remain `false` for `singleton` and `transient` services resolved through fallback.
|
|
186
|
+
|
|
187
|
+
### `container.reset(strategy?)`
|
|
188
|
+
|
|
189
|
+
Supported strategies:
|
|
190
|
+
|
|
191
|
+
- `'value'` clears cached instances but keeps registrations;
|
|
192
|
+
- `'service'` removes registrations from the current container.
|
|
193
|
+
|
|
194
|
+
This is especially useful in tests.
|
|
195
|
+
|
|
196
|
+
## Internal architecture
|
|
197
|
+
|
|
198
|
+
The implementation is intentionally small and split into a few focused modules:
|
|
199
|
+
|
|
200
|
+
- `src/decorators/` records decorator metadata for services and injected fields.
|
|
201
|
+
- `src/container/container.ts` stores service metadata and performs resolution.
|
|
202
|
+
- `src/container/registry.ts` owns the default container and named-container registry.
|
|
203
|
+
- `src/types/` defines service identifiers, scopes, metadata, and injection metadata.
|
|
204
|
+
- `src/errors/` provides explicit runtime error classes.
|
|
205
|
+
- `test/` exercises registration, scoping, fallback behavior, reset behavior, and error paths.
|
|
206
|
+
|
|
207
|
+
Resolution flow at a high level:
|
|
208
|
+
|
|
209
|
+
1. decorators attach injection metadata through `context.metadata`;
|
|
210
|
+
2. `@Service()` registers the class and its collected injections in the default container;
|
|
211
|
+
3. `container.get()` loads registration metadata, handles scope rules, and creates the instance;
|
|
212
|
+
4. each injected field is resolved recursively from the same container after instance construction;
|
|
213
|
+
5. the container tracks the current resolution path to detect circular dependencies.
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
Install dependencies:
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
bun install
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Available scripts:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
bun run build
|
|
227
|
+
bun run typecheck
|
|
228
|
+
bun run test
|
|
229
|
+
bun run lint
|
|
230
|
+
bun run fmt
|
|
231
|
+
bun run fmt:check
|
|
232
|
+
bun run hooks:install
|
|
233
|
+
bun run hooks:validate
|
|
234
|
+
bun run hooks:run:pre-commit
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
What they do:
|
|
238
|
+
|
|
239
|
+
- `build`: compile the package with `tsc -p tsconfig.build.json`
|
|
240
|
+
- `typecheck`: run the TypeScript compiler in check mode
|
|
241
|
+
- `test`: run Bun tests
|
|
242
|
+
- `lint`: run `oxlint` with warnings denied
|
|
243
|
+
- `fmt`: format the repository with `oxfmt`
|
|
244
|
+
- `fmt:check`: verify formatting without writing changes
|
|
245
|
+
- `hooks:*`: install, validate, or run the Lefthook-based Git hooks
|
|
246
|
+
|
|
247
|
+
`bun install` also triggers `postinstall`, which installs the local Git hooks automatically.
|
|
248
|
+
|
|
249
|
+
Current note: `typecheck` uses `tsconfig.json` with `noEmit: true`, while `build` uses `tsconfig.build.json` with `noEmit: false` to emit `dist/` and declaration files.
|
|
250
|
+
|
|
251
|
+
## Local quality gates
|
|
252
|
+
|
|
253
|
+
The repository currently enforces:
|
|
254
|
+
|
|
255
|
+
- strict TypeScript checking;
|
|
256
|
+
- `oxlint` for linting;
|
|
257
|
+
- `oxfmt` for formatting;
|
|
258
|
+
- Lefthook `pre-commit` checks for staged TypeScript, JavaScript, Markdown, YAML, and YML files.
|
|
259
|
+
|
|
260
|
+
The package `prepack` script runs lint, format check, typecheck, and build before publishing.
|
|
261
|
+
|
|
262
|
+
## Repository layout
|
|
263
|
+
|
|
264
|
+
```text
|
|
265
|
+
src/
|
|
266
|
+
container/
|
|
267
|
+
decorators/
|
|
268
|
+
errors/
|
|
269
|
+
types/
|
|
270
|
+
test/
|
|
271
|
+
dist/
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- `src/` contains the library source.
|
|
275
|
+
- `test/` contains Bun test coverage for the runtime behavior.
|
|
276
|
+
- `dist/` contains the built ESM output and declaration files emitted by `tsconfig.build.json`.
|
|
277
|
+
|
|
278
|
+
## Community
|
|
279
|
+
|
|
280
|
+
- Contributing guide: [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
281
|
+
- Code of Conduct: [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
|
282
|
+
- Security policy: [SECURITY.md](./SECURITY.md)
|
|
283
|
+
- License: [LICENSE](./LICENSE)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are applied to the latest development line first.
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
| ------------------------- | --------- |
|
|
9
|
+
| `0.x` | Yes |
|
|
10
|
+
| Earlier pre-release lines | No |
|
|
11
|
+
|
|
12
|
+
## Reporting a vulnerability
|
|
13
|
+
|
|
14
|
+
Please report vulnerabilities through GitHub Security Advisories for this repository or contact the maintainers privately. Do not open a public issue for an unpatched security problem.
|
|
15
|
+
|
|
16
|
+
## Scope notes
|
|
17
|
+
|
|
18
|
+
Reports are especially helpful when they involve:
|
|
19
|
+
|
|
20
|
+
- unexpected code execution through provider factories or container hooks;
|
|
21
|
+
- token collisions or registry behavior that can resolve the wrong dependency;
|
|
22
|
+
- metadata leaks that expose implementation details across package boundaries; or
|
|
23
|
+
- denial-of-service style resolution loops that can be triggered by untrusted input.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ContainerIdentifier, Metadata, ServiceIdentifier } from '../types';
|
|
2
|
+
export declare class Container {
|
|
3
|
+
readonly id: ContainerIdentifier;
|
|
4
|
+
private metadataMap;
|
|
5
|
+
private resolving;
|
|
6
|
+
private resolvingPath;
|
|
7
|
+
constructor(id: ContainerIdentifier);
|
|
8
|
+
static of(id?: ContainerIdentifier): Container;
|
|
9
|
+
set<T>(metadata: Metadata<T>): void;
|
|
10
|
+
has(id: ServiceIdentifier): boolean;
|
|
11
|
+
reset(strategy?: 'value' | 'service'): void;
|
|
12
|
+
get<T>(id: ServiceIdentifier<T>): T;
|
|
13
|
+
private isDefault;
|
|
14
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { CircularDependencyError, ServiceNotFoundError } from '../errors';
|
|
2
|
+
import { EMPTY_VALUE } from '../types';
|
|
3
|
+
import { ContainerRegistry } from './registry';
|
|
4
|
+
export class Container {
|
|
5
|
+
id;
|
|
6
|
+
metadataMap = new Map();
|
|
7
|
+
resolving = new Set();
|
|
8
|
+
resolvingPath = [];
|
|
9
|
+
constructor(id) {
|
|
10
|
+
this.id = id;
|
|
11
|
+
}
|
|
12
|
+
static of(id = 'default') {
|
|
13
|
+
if (id === 'default') {
|
|
14
|
+
return ContainerRegistry.defaultContainer;
|
|
15
|
+
}
|
|
16
|
+
if (ContainerRegistry.hasContainer(id)) {
|
|
17
|
+
return ContainerRegistry.getContainer(id);
|
|
18
|
+
}
|
|
19
|
+
const container = new Container(id);
|
|
20
|
+
ContainerRegistry.registerContainer(container);
|
|
21
|
+
return container;
|
|
22
|
+
}
|
|
23
|
+
set(metadata) {
|
|
24
|
+
if (metadata.scope === 'singleton' && !this.isDefault()) {
|
|
25
|
+
ContainerRegistry.defaultContainer.set(metadata);
|
|
26
|
+
this.metadataMap.delete(metadata.id);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const newMetadata = {
|
|
30
|
+
...metadata,
|
|
31
|
+
};
|
|
32
|
+
const existingMetadata = this.metadataMap.get(newMetadata.id);
|
|
33
|
+
if (existingMetadata) {
|
|
34
|
+
Object.assign(existingMetadata, newMetadata);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.metadataMap.set(newMetadata.id, newMetadata);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
has(id) {
|
|
41
|
+
return this.metadataMap.has(id);
|
|
42
|
+
}
|
|
43
|
+
reset(strategy = 'value') {
|
|
44
|
+
if (strategy === 'value') {
|
|
45
|
+
this.metadataMap.forEach((metadata) => {
|
|
46
|
+
metadata.value = EMPTY_VALUE;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.metadataMap.clear();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
get(id) {
|
|
54
|
+
let metadata = this.metadataMap.get(id);
|
|
55
|
+
if (!metadata && !this.isDefault()) {
|
|
56
|
+
const defaultMetadata = ContainerRegistry.defaultContainer.metadataMap.get(id);
|
|
57
|
+
if (!defaultMetadata) {
|
|
58
|
+
throw new ServiceNotFoundError(id);
|
|
59
|
+
}
|
|
60
|
+
if (defaultMetadata.scope === 'singleton') {
|
|
61
|
+
return ContainerRegistry.defaultContainer.get(id);
|
|
62
|
+
}
|
|
63
|
+
if (defaultMetadata.scope === 'container') {
|
|
64
|
+
metadata = {
|
|
65
|
+
...defaultMetadata,
|
|
66
|
+
injections: [...defaultMetadata.injections],
|
|
67
|
+
value: EMPTY_VALUE,
|
|
68
|
+
};
|
|
69
|
+
this.metadataMap.set(id, metadata);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
metadata = defaultMetadata;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!metadata) {
|
|
76
|
+
throw new ServiceNotFoundError(id);
|
|
77
|
+
}
|
|
78
|
+
if (metadata.scope !== 'transient' && metadata.value !== EMPTY_VALUE) {
|
|
79
|
+
return metadata.value;
|
|
80
|
+
}
|
|
81
|
+
if (this.resolving.has(id)) {
|
|
82
|
+
throw new CircularDependencyError([...this.resolvingPath, id]);
|
|
83
|
+
}
|
|
84
|
+
this.resolving.add(id);
|
|
85
|
+
this.resolvingPath.push(id);
|
|
86
|
+
try {
|
|
87
|
+
const instance = new metadata.Class();
|
|
88
|
+
for (const injection of metadata.injections) {
|
|
89
|
+
Object.defineProperty(instance, injection.name, {
|
|
90
|
+
value: this.get(injection.id),
|
|
91
|
+
writable: true,
|
|
92
|
+
configurable: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (metadata.scope !== 'transient') {
|
|
96
|
+
metadata.value = instance;
|
|
97
|
+
}
|
|
98
|
+
return instance;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
this.resolving.delete(id);
|
|
102
|
+
this.resolvingPath.pop();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
isDefault() {
|
|
106
|
+
return this === ContainerRegistry.defaultContainer;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Container } from './container';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Container } from './container';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ContainerIdentifier } from '../types';
|
|
2
|
+
import { Container } from './container';
|
|
3
|
+
export declare class ContainerRegistry {
|
|
4
|
+
private static defaultContainerInstance?;
|
|
5
|
+
private static readonly containerMap;
|
|
6
|
+
static get defaultContainer(): Container;
|
|
7
|
+
static registerContainer(container: Container): void;
|
|
8
|
+
static getContainer(id: ContainerIdentifier): Container | undefined;
|
|
9
|
+
static hasContainer(id: ContainerIdentifier): boolean;
|
|
10
|
+
static removeContainer(id: ContainerIdentifier): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ContainerDuplicatedError, DefaultContainerIdError } from '../errors';
|
|
2
|
+
import { Container } from './container';
|
|
3
|
+
export class ContainerRegistry {
|
|
4
|
+
static defaultContainerInstance;
|
|
5
|
+
static containerMap = new Map();
|
|
6
|
+
static get defaultContainer() {
|
|
7
|
+
this.defaultContainerInstance ??= new Container('default');
|
|
8
|
+
return this.defaultContainerInstance;
|
|
9
|
+
}
|
|
10
|
+
static registerContainer(container) {
|
|
11
|
+
if (container.id === 'default') {
|
|
12
|
+
throw new DefaultContainerIdError();
|
|
13
|
+
}
|
|
14
|
+
if (ContainerRegistry.containerMap.has(container.id)) {
|
|
15
|
+
throw new ContainerDuplicatedError(container.id);
|
|
16
|
+
}
|
|
17
|
+
this.containerMap.set(container.id, container);
|
|
18
|
+
}
|
|
19
|
+
static getContainer(id) {
|
|
20
|
+
if (id === 'default') {
|
|
21
|
+
return this.defaultContainer;
|
|
22
|
+
}
|
|
23
|
+
return this.containerMap.get(id);
|
|
24
|
+
}
|
|
25
|
+
static hasContainer(id) {
|
|
26
|
+
if (id === 'default') {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return this.containerMap.has(id);
|
|
30
|
+
}
|
|
31
|
+
static removeContainer(id) {
|
|
32
|
+
if (id === 'default') {
|
|
33
|
+
throw new DefaultContainerIdError();
|
|
34
|
+
}
|
|
35
|
+
this.containerMap.delete(id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ContainerRegistry } from '../container/registry';
|
|
2
|
+
import { INJECTION_KEY, EMPTY_VALUE } from '../types';
|
|
3
|
+
export function Service(options) {
|
|
4
|
+
return function (target, context) {
|
|
5
|
+
const injections = (context.metadata[INJECTION_KEY] ?? []);
|
|
6
|
+
ContainerRegistry.defaultContainer.set({
|
|
7
|
+
id: options?.id ?? target,
|
|
8
|
+
Class: target,
|
|
9
|
+
name: context.name,
|
|
10
|
+
injections,
|
|
11
|
+
scope: options?.scope ?? 'container',
|
|
12
|
+
value: EMPTY_VALUE,
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CircularDependencyError } from './circular-dependency-error';
|
|
2
|
+
export { ContainerDuplicatedError } from './container-duplicated-error';
|
|
3
|
+
export { DefaultContainerIdError } from './default-container-id-error';
|
|
4
|
+
export { ServiceNotFoundError } from './service-not-found-error';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CircularDependencyError } from './circular-dependency-error';
|
|
2
|
+
export { ContainerDuplicatedError } from './container-duplicated-error';
|
|
3
|
+
export { DefaultContainerIdError } from './default-container-id-error';
|
|
4
|
+
export { ServiceNotFoundError } from './service-not-found-error';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Constructable } from './constructable';
|
|
2
|
+
import type { InjectionMetadata } from './injection';
|
|
3
|
+
import type { ServiceIdentifier, ServiceScope } from './service';
|
|
4
|
+
export type ContainerIdentifier = string | symbol;
|
|
5
|
+
export declare const EMPTY_VALUE: unique symbol;
|
|
6
|
+
export interface Metadata<T = unknown> {
|
|
7
|
+
id: ServiceIdentifier;
|
|
8
|
+
Class: Constructable<T>;
|
|
9
|
+
name?: string | symbol;
|
|
10
|
+
injections: InjectionMetadata[];
|
|
11
|
+
scope: ServiceScope;
|
|
12
|
+
value: T | typeof EMPTY_VALUE;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const EMPTY_VALUE = Symbol.for('EMPTY_VALUE');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { AbstractConstructable, Constructable } from './constructable';
|
|
2
|
+
export type { ServiceIdentifier, ServiceOption } from './service';
|
|
3
|
+
export { type ContainerIdentifier, type Metadata, EMPTY_VALUE } from './container';
|
|
4
|
+
export { type InjectionMetadata, INJECTION_KEY } from './injection';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const INJECTION_KEY = Symbol.for('navi-di:injections');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AbstractConstructable, Constructable } from './constructable';
|
|
2
|
+
export type ServiceIdentifier<T = unknown, Args extends unknown[] = never[]> = Constructable<T, Args> | AbstractConstructable<T, Args> | CallableFunction | string;
|
|
3
|
+
export type ServiceScope = 'singleton' | 'container' | 'transient';
|
|
4
|
+
export interface ServiceOption {
|
|
5
|
+
id?: ServiceIdentifier;
|
|
6
|
+
scope?: ServiceScope;
|
|
7
|
+
}
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "navi-di",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dependency injection for standard ECMAScript decorators.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"decorators",
|
|
8
|
+
"dependency-injection",
|
|
9
|
+
"di",
|
|
10
|
+
"ecmascript",
|
|
11
|
+
"node",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/Naviary-Sanctuary/navi-di#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Naviary-Sanctuary/navi-di/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Naviary-Sanctuary/navi-di.git"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"CONTRIBUTING.md",
|
|
28
|
+
"SECURITY.md",
|
|
29
|
+
"CODE_OF_CONDUCT.md"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
48
|
+
"hooks:install": "lefthook install",
|
|
49
|
+
"hooks:validate": "lefthook validate",
|
|
50
|
+
"hooks:run:pre-commit": "lefthook run pre-commit --all-files --force",
|
|
51
|
+
"lint": "oxlint . --deny-warnings",
|
|
52
|
+
"fmt": "oxfmt --config .oxfmt.json .",
|
|
53
|
+
"fmt:check": "oxfmt --check --config .oxfmt.json .",
|
|
54
|
+
"postinstall": "lefthook install",
|
|
55
|
+
"prepack": "bun run lint && bun run fmt:check && bun run typecheck && bun run build",
|
|
56
|
+
"test": "bun test --pass-with-no-tests"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/bun": "1.3.11",
|
|
60
|
+
"lefthook": "^2.1.4",
|
|
61
|
+
"oxfmt": "^0.41.0",
|
|
62
|
+
"oxlint": "^1.56.0",
|
|
63
|
+
"typescript": "^5.9.3"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"bun": ">=1.3.0",
|
|
67
|
+
"node": ">=18.18"
|
|
68
|
+
},
|
|
69
|
+
"packageManager": "bun@1.3.10"
|
|
70
|
+
}
|