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.
Files changed (40) hide show
  1. package/CODE_OF_CONDUCT.md +20 -0
  2. package/CONTRIBUTING.md +28 -0
  3. package/LICENSE +21 -0
  4. package/README.md +283 -0
  5. package/SECURITY.md +23 -0
  6. package/dist/container/container.d.ts +14 -0
  7. package/dist/container/container.js +108 -0
  8. package/dist/container/index.d.ts +1 -0
  9. package/dist/container/index.js +1 -0
  10. package/dist/container/registry.d.ts +11 -0
  11. package/dist/container/registry.js +37 -0
  12. package/dist/decorators/index.d.ts +2 -0
  13. package/dist/decorators/index.js +2 -0
  14. package/dist/decorators/inject.d.ts +2 -0
  15. package/dist/decorators/inject.js +10 -0
  16. package/dist/decorators/service.d.ts +3 -0
  17. package/dist/decorators/service.js +15 -0
  18. package/dist/errors/circular-dependency-error.d.ts +5 -0
  19. package/dist/errors/circular-dependency-error.js +6 -0
  20. package/dist/errors/container-duplicated-error.d.ts +5 -0
  21. package/dist/errors/container-duplicated-error.js +6 -0
  22. package/dist/errors/default-container-id-error.d.ts +4 -0
  23. package/dist/errors/default-container-id-error.js +6 -0
  24. package/dist/errors/index.d.ts +4 -0
  25. package/dist/errors/index.js +4 -0
  26. package/dist/errors/service-not-found-error.d.ts +5 -0
  27. package/dist/errors/service-not-found-error.js +6 -0
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.js +2 -0
  30. package/dist/types/constructable.d.ts +2 -0
  31. package/dist/types/constructable.js +0 -0
  32. package/dist/types/container.d.ts +13 -0
  33. package/dist/types/container.js +1 -0
  34. package/dist/types/index.d.ts +4 -0
  35. package/dist/types/index.js +2 -0
  36. package/dist/types/injection.d.ts +6 -0
  37. package/dist/types/injection.js +1 -0
  38. package/dist/types/service.d.ts +7 -0
  39. package/dist/types/service.js +0 -0
  40. 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).
@@ -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,2 @@
1
+ export { Inject } from './inject';
2
+ export { Service } from './service';
@@ -0,0 +1,2 @@
1
+ export { Inject } from './inject';
2
+ export { Service } from './service';
@@ -0,0 +1,2 @@
1
+ import { type Constructable } from '../types';
2
+ export declare function Inject<T>(dependency: Constructable<T>): (_: undefined, context: ClassFieldDecoratorContext) => void;
@@ -0,0 +1,10 @@
1
+ import { INJECTION_KEY } from '../types';
2
+ export function Inject(dependency) {
3
+ return function (_, context) {
4
+ const injections = (context.metadata[INJECTION_KEY] ??= []);
5
+ injections.push({
6
+ id: dependency,
7
+ name: context.name,
8
+ });
9
+ };
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { ServiceOption } from '../types';
2
+ export declare function Service(): Function;
3
+ export declare function Service(options?: ServiceOption): Function;
@@ -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,5 @@
1
+ import type { ServiceIdentifier } from '../types';
2
+ export declare class CircularDependencyError extends Error {
3
+ name: string;
4
+ constructor(path: ServiceIdentifier[]);
5
+ }
@@ -0,0 +1,6 @@
1
+ export class CircularDependencyError extends Error {
2
+ name = 'CircularDependencyError';
3
+ constructor(path) {
4
+ super(`Circular dependency detected: ${path.map(String).join(' -> ')}`);
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ import type { ContainerIdentifier } from '../types';
2
+ export declare class ContainerDuplicatedError extends Error {
3
+ name: string;
4
+ constructor(id: ContainerIdentifier);
5
+ }
@@ -0,0 +1,6 @@
1
+ export class ContainerDuplicatedError extends Error {
2
+ name = 'ContainerDuplicatedError';
3
+ constructor(id) {
4
+ super(`Cannot register container with same ID(${String(id)})`);
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ export declare class DefaultContainerIdError extends Error {
2
+ name: string;
3
+ constructor();
4
+ }
@@ -0,0 +1,6 @@
1
+ export class DefaultContainerIdError extends Error {
2
+ name = 'DefaultContainerIdError';
3
+ constructor() {
4
+ super(`You cannot register a container with the "default" for ID`);
5
+ }
6
+ }
@@ -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';
@@ -0,0 +1,5 @@
1
+ import type { ServiceIdentifier } from '../types';
2
+ export declare class ServiceNotFoundError extends Error {
3
+ name: string;
4
+ constructor(id: ServiceIdentifier);
5
+ }
@@ -0,0 +1,6 @@
1
+ export class ServiceNotFoundError extends Error {
2
+ name = 'ServiceNotFoundError';
3
+ constructor(id) {
4
+ super(`Service not found: ${String(id)}`);
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ export { Container } from './container';
2
+ export { Service, Inject } from './decorators';
3
+ export type { AbstractConstructable, Constructable } from './types/constructable.ts';
4
+ export type { ServiceIdentifier } from './types/service.ts';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Container } from './container';
2
+ export { Service, Inject } from './decorators';
@@ -0,0 +1,2 @@
1
+ export type Constructable<T = object, Args extends unknown[] = never[]> = new (...args: Args) => T;
2
+ export type AbstractConstructable<T = object, Args extends unknown[] = never[]> = abstract new (...args: Args) => T;
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,2 @@
1
+ export { EMPTY_VALUE } from './container';
2
+ export { INJECTION_KEY } from './injection';
@@ -0,0 +1,6 @@
1
+ import type { ServiceIdentifier } from './service';
2
+ export interface InjectionMetadata {
3
+ id: ServiceIdentifier;
4
+ name: string | symbol;
5
+ }
6
+ export declare const INJECTION_KEY: unique symbol;
@@ -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
+ }