alloy-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/README.md +69 -0
- package/dist/lib/container.d.ts +117 -0
- package/dist/lib/container.js +266 -0
- package/dist/lib/decorators.d.ts +177 -0
- package/dist/lib/decorators.js +126 -0
- package/dist/lib/dependency-error.js +31 -0
- package/dist/lib/env-detection.js +44 -0
- package/dist/lib/lazy.d.ts +39 -0
- package/dist/lib/lazy.js +18 -0
- package/dist/lib/providers.d.ts +112 -0
- package/dist/lib/providers.js +166 -0
- package/dist/lib/scope.d.ts +8 -0
- package/dist/lib/scope.js +8 -0
- package/dist/lib/service-identifiers.d.ts +34 -0
- package/dist/lib/service-identifiers.js +54 -0
- package/dist/lib/testing/mocking.d.ts +14 -0
- package/dist/lib/testing/mocking.js +109 -0
- package/dist/lib/testing/registry.js +19 -0
- package/dist/lib/types.d.ts +22 -0
- package/dist/lib/types.js +21 -0
- package/dist/plugins/core/codegen.js +288 -0
- package/dist/plugins/core/decorators.js +90 -0
- package/dist/plugins/core/discovery-store.js +78 -0
- package/dist/plugins/core/identifier-resolver.js +22 -0
- package/dist/plugins/core/lazy.js +98 -0
- package/dist/plugins/core/scanner.js +93 -0
- package/dist/plugins/core/types.d.ts +44 -0
- package/dist/plugins/core/utils.js +45 -0
- package/dist/plugins/rollup-plugin/build-utils.js +49 -0
- package/dist/plugins/rollup-plugin/index.d.ts +30 -0
- package/dist/plugins/rollup-plugin/index.js +225 -0
- package/dist/plugins/vite-plugin/index.d.ts +25 -0
- package/dist/plugins/vite-plugin/index.js +154 -0
- package/dist/plugins/vite-plugin/manifest-utils.js +213 -0
- package/dist/rollup.d.ts +2 -0
- package/dist/rollup.js +7 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +8 -0
- package/dist/test.d.ts +50 -0
- package/dist/test.js +67 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +7 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# alloy-di
|
|
2
|
+
|
|
3
|
+
`alloy-di` is a compile-time dependency injection toolkit for Vite. It scans your TypeScript during build, generates a static container, and ships a tiny runtime so you get dependency injection without reflection overhead.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- **Build-time graph** – services, scopes, and dependencies are resolved while bundling, so runtime work stays minimal.
|
|
8
|
+
- **First-class lazy loading** – use `Lazy()` or provider-based lazy registrations to keep optional features in separate chunks.
|
|
9
|
+
- **Framework agnostic** – works anywhere Vite runs: React, Vue, Svelte, SSR, libraries, and plain TS apps.
|
|
10
|
+
- **Type safe** – generates `serviceIdentifiers` and manifest declarations for precise inference.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add -D alloy-di
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 30‑second setup
|
|
19
|
+
|
|
20
|
+
1. **Add the Vite plugin**
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { defineConfig } from "vite";
|
|
24
|
+
import alloy from "alloy-di/vite";
|
|
25
|
+
|
|
26
|
+
export default defineConfig({
|
|
27
|
+
plugins: [alloy()],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. **Annotate services**
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Injectable, Singleton, deps } from "alloy-di/runtime";
|
|
35
|
+
|
|
36
|
+
@Singleton()
|
|
37
|
+
export class ServiceA {}
|
|
38
|
+
|
|
39
|
+
@Injectable(deps(ServiceA))
|
|
40
|
+
export class AppService {
|
|
41
|
+
constructor(private readonly serviceA: ServiceA) {}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. **Resolve from the virtual container**
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import container, { serviceIdentifiers } from "virtual:alloy-container";
|
|
49
|
+
|
|
50
|
+
const app = await container.get(serviceIdentifiers.AppService);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Need manifests, providers, or testing utilities? See the docs site for complete guides.
|
|
54
|
+
|
|
55
|
+
## Documentation
|
|
56
|
+
|
|
57
|
+
- **Website**: https://alloy-di.dev (generated from `/docs`)
|
|
58
|
+
- **Develop locally**: `pnpm docs:dev`
|
|
59
|
+
- **Build static site**: `pnpm docs:build`
|
|
60
|
+
|
|
61
|
+
The site covers getting started, plugin options, manifest authoring, lazy loading, testing helpers, and architecture deep dives.
|
|
62
|
+
|
|
63
|
+
## Examples in this repo
|
|
64
|
+
|
|
65
|
+
- `packages/examples/app` – React + Vite app consuming decorated services, manifests, and providers.
|
|
66
|
+
- `packages/examples/library-internal` – monorepo library that emits `alloy.manifest.mjs` via the Rolldown plugin.
|
|
67
|
+
- `packages/examples/library-external` – plain classes registered through providers.
|
|
68
|
+
|
|
69
|
+
Clone the repo, run `pnpm install`, then `pnpm --filter @alloy-di/example-app dev` to explore.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Constructor, Newable, Token } from "./types.js";
|
|
2
|
+
import { ServiceIdentifier } from "./service-identifiers.js";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/container.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runtime dependency injection container used by generated modules and tests.
|
|
8
|
+
*
|
|
9
|
+
* It stores metadata discovered at build time, resolves constructor dependencies,
|
|
10
|
+
* performs singleton caching, and supports token-based value providers.
|
|
11
|
+
*/
|
|
12
|
+
declare class Container {
|
|
13
|
+
private singletons;
|
|
14
|
+
private pendingSingletons;
|
|
15
|
+
private instanceOverrides;
|
|
16
|
+
private metadataCache;
|
|
17
|
+
private valueProviders;
|
|
18
|
+
private factoryWarningCache;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve (and construct) the requested service.
|
|
21
|
+
*
|
|
22
|
+
* @param target - Class constructor that was decorated with `@Injectable`/`@Singleton`.
|
|
23
|
+
* @returns A promise that resolves to the instantiated service.
|
|
24
|
+
*/
|
|
25
|
+
get<T>(target: Newable<T>): Promise<T>;
|
|
26
|
+
get<T>(identifier: ServiceIdentifier<T>): Promise<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Provide a concrete instance override for a class constructor.
|
|
29
|
+
* Used by test utilities to inject mocks/stubs without altering global metadata.
|
|
30
|
+
*/
|
|
31
|
+
overrideInstance<T>(target: Newable<T>, instance: T): void;
|
|
32
|
+
/**
|
|
33
|
+
* Retrieve the stable identifier associated with a constructor.
|
|
34
|
+
* Consumers can cache this and later call {@link getByIdentifier}.
|
|
35
|
+
*/
|
|
36
|
+
getIdentifier<T>(target: Constructor): ServiceIdentifier<T>;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a service using its stable identifier.
|
|
39
|
+
* Identifiers remain safe across minification and code splitting.
|
|
40
|
+
*/
|
|
41
|
+
getByIdentifier<T = unknown>(identifier: ServiceIdentifier<T>): Promise<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Register a concrete value for an injection token at runtime.
|
|
44
|
+
*
|
|
45
|
+
* @param token - The token created via `createToken`.
|
|
46
|
+
* @param value - The value that should be injected when the token is requested.
|
|
47
|
+
*/
|
|
48
|
+
provideValue<T>(token: Token<T>, value: T): void;
|
|
49
|
+
/**
|
|
50
|
+
* Retrieve a provided value for a token from this container.
|
|
51
|
+
* Throws if no provider is registered for the token.
|
|
52
|
+
*/
|
|
53
|
+
getToken<T>(token: Token<T>): T;
|
|
54
|
+
private getByConstructor;
|
|
55
|
+
private maybeWarnFactoryLazyConstructorUsage;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a constructor, managing singleton lifetimes and detecting circular dependencies.
|
|
58
|
+
* This is the core resolution logic that orchestrates caching, coalescing, and instantiation.
|
|
59
|
+
*
|
|
60
|
+
* @param target - Service constructor to resolve
|
|
61
|
+
* @param resolutionStack - Chain of services currently being resolved (for cycle detection)
|
|
62
|
+
* @returns Promise resolving to the service instance
|
|
63
|
+
* @throws Error if a circular dependency is detected
|
|
64
|
+
*/
|
|
65
|
+
private resolve;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a singleton service with caching and in-flight creation coalescing.
|
|
68
|
+
*/
|
|
69
|
+
private resolveSingleton;
|
|
70
|
+
/**
|
|
71
|
+
* Instantiate a class by resolving and injecting all declared dependencies.
|
|
72
|
+
* Handles factory-lazy services by importing the real class before instantiation.
|
|
73
|
+
*
|
|
74
|
+
* @param target - Service constructor (may be a stub class if factory provided)
|
|
75
|
+
* @param dependencies - Array of dependency items to resolve and inject
|
|
76
|
+
* @param resolutionStack - Current resolution chain
|
|
77
|
+
* @param factory - Optional lazy factory to import the real class
|
|
78
|
+
* @returns Promise resolving to the instantiated service
|
|
79
|
+
*/
|
|
80
|
+
private createInstance;
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a single dependency entry, handling lazies, tokens, and constructors.
|
|
83
|
+
* This is called for each parameter in a service's dependency array.
|
|
84
|
+
*
|
|
85
|
+
* @param param - Dependency item (can be Lazy, Token, or Constructor)
|
|
86
|
+
* @param target - Service being constructed (for error messages)
|
|
87
|
+
* @param resolutionStack - Current resolution chain
|
|
88
|
+
* @returns Promise resolving to the dependency instance
|
|
89
|
+
* @throws Error if dependency type is invalid
|
|
90
|
+
*/
|
|
91
|
+
private resolveParam;
|
|
92
|
+
/**
|
|
93
|
+
* Execute a lazy importer with optional retry/backoff semantics.
|
|
94
|
+
* Implements exponential backoff for transient network failures.
|
|
95
|
+
*
|
|
96
|
+
* @param lazyDep - Lazy dependency wrapper with importer function and retry config
|
|
97
|
+
* @param target - Service being resolved (for error messages)
|
|
98
|
+
* @param resolutionStack - Current resolution chain (for cycle detection and error context)
|
|
99
|
+
* @returns The imported class constructor
|
|
100
|
+
* @throws Error if all retry attempts exhausted or import returns non-constructor
|
|
101
|
+
*/
|
|
102
|
+
private importWithRetry;
|
|
103
|
+
/**
|
|
104
|
+
* Resolve a token dependency via registered value providers.
|
|
105
|
+
*/
|
|
106
|
+
private resolveTokenLike;
|
|
107
|
+
/**
|
|
108
|
+
* Format a readable representation of the resolution stack for error messages.
|
|
109
|
+
*/
|
|
110
|
+
private formatStackPath;
|
|
111
|
+
/**
|
|
112
|
+
* Retrieve (and memoize) the DI metadata for a service from the registry.
|
|
113
|
+
*/
|
|
114
|
+
private getServiceMetadata;
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { Container };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { ServiceScope } from "./scope.js";
|
|
2
|
+
import { isConstructor, isToken } from "./types.js";
|
|
3
|
+
import { isLazy } from "./lazy.js";
|
|
4
|
+
import { dependenciesRegistry } from "./decorators.js";
|
|
5
|
+
import { DependencyResolutionError } from "./dependency-error.js";
|
|
6
|
+
import { getConstructorByIdentifier, getServiceIdentifier } from "./service-identifiers.js";
|
|
7
|
+
import { isDevEnvironment } from "./env-detection.js";
|
|
8
|
+
|
|
9
|
+
//#region src/lib/container.ts
|
|
10
|
+
function classifyDependency(value) {
|
|
11
|
+
if (isLazy(value)) return {
|
|
12
|
+
kind: "lazy",
|
|
13
|
+
lazy: value
|
|
14
|
+
};
|
|
15
|
+
if (isToken(value)) return {
|
|
16
|
+
kind: "token",
|
|
17
|
+
token: value
|
|
18
|
+
};
|
|
19
|
+
if (isConstructor(value)) return {
|
|
20
|
+
kind: "constructor",
|
|
21
|
+
ctor: value
|
|
22
|
+
};
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function hasFactory(metadata) {
|
|
26
|
+
return Boolean(metadata?.factory);
|
|
27
|
+
}
|
|
28
|
+
function isProviderPlaceholder(target) {
|
|
29
|
+
return Boolean(typeof target === "function" && "__alloyLazy" in target && target.__alloyLazy === true);
|
|
30
|
+
}
|
|
31
|
+
function formatFactoryLazyWarning(target) {
|
|
32
|
+
return `[alloy] container.get(${target.name || "<anonymous>"}) resolved a factory-lazy service via constructor. Use container.get(${target.name ? `serviceIdentifiers.${target.name}` : "serviceIdentifiers.<Service>"}) or cache const id = ${target.name ? `container.getIdentifier(${target.name})` : `container.getIdentifier(<Service>)`}; container.get(id) to preserve lazy loading.`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Runtime dependency injection container used by generated modules and tests.
|
|
36
|
+
*
|
|
37
|
+
* It stores metadata discovered at build time, resolves constructor dependencies,
|
|
38
|
+
* performs singleton caching, and supports token-based value providers.
|
|
39
|
+
*/
|
|
40
|
+
var Container = class {
|
|
41
|
+
singletons = /* @__PURE__ */ new Map();
|
|
42
|
+
pendingSingletons = /* @__PURE__ */ new Map();
|
|
43
|
+
instanceOverrides = /* @__PURE__ */ new Map();
|
|
44
|
+
metadataCache = /* @__PURE__ */ new Map();
|
|
45
|
+
valueProviders = /* @__PURE__ */ new Map();
|
|
46
|
+
factoryWarningCache = /* @__PURE__ */ new WeakSet();
|
|
47
|
+
async get(targetOrIdentifier) {
|
|
48
|
+
if (typeof targetOrIdentifier === "symbol") return this.getByIdentifier(targetOrIdentifier);
|
|
49
|
+
return this.getByConstructor(targetOrIdentifier);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Provide a concrete instance override for a class constructor.
|
|
53
|
+
* Used by test utilities to inject mocks/stubs without altering global metadata.
|
|
54
|
+
*/
|
|
55
|
+
overrideInstance(target, instance) {
|
|
56
|
+
this.instanceOverrides.set(target, instance);
|
|
57
|
+
this.singletons.set(target, instance);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Retrieve the stable identifier associated with a constructor.
|
|
61
|
+
* Consumers can cache this and later call {@link getByIdentifier}.
|
|
62
|
+
*/
|
|
63
|
+
getIdentifier(target) {
|
|
64
|
+
return getServiceIdentifier(target);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a service using its stable identifier.
|
|
68
|
+
* Identifiers remain safe across minification and code splitting.
|
|
69
|
+
*/
|
|
70
|
+
async getByIdentifier(identifier) {
|
|
71
|
+
const ctor = getConstructorByIdentifier(identifier);
|
|
72
|
+
if (!ctor) throw new Error(`No service registered for identifier ${identifier.description ?? identifier.toString()}`);
|
|
73
|
+
return this.getByConstructor(ctor, { skipFactoryWarning: true });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Register a concrete value for an injection token at runtime.
|
|
77
|
+
*
|
|
78
|
+
* @param token - The token created via `createToken`.
|
|
79
|
+
* @param value - The value that should be injected when the token is requested.
|
|
80
|
+
*/
|
|
81
|
+
provideValue(token, value) {
|
|
82
|
+
this.valueProviders.set(token.id, value);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Retrieve a provided value for a token from this container.
|
|
86
|
+
* Throws if no provider is registered for the token.
|
|
87
|
+
*/
|
|
88
|
+
getToken(token) {
|
|
89
|
+
if (!this.valueProviders.has(token.id)) throw new Error(`No provider registered for token ${token.description ?? String(token.id)}`);
|
|
90
|
+
return this.valueProviders.get(token.id);
|
|
91
|
+
}
|
|
92
|
+
async getByConstructor(target, options) {
|
|
93
|
+
if (!options?.skipFactoryWarning) this.maybeWarnFactoryLazyConstructorUsage(target);
|
|
94
|
+
return this.resolve(target, []);
|
|
95
|
+
}
|
|
96
|
+
maybeWarnFactoryLazyConstructorUsage(target) {
|
|
97
|
+
if (!isDevEnvironment()) return;
|
|
98
|
+
if (!hasFactory(this.metadataCache.get(target) ?? this.getServiceMetadata(target)) || this.factoryWarningCache.has(target) || isProviderPlaceholder(target)) return;
|
|
99
|
+
this.factoryWarningCache.add(target);
|
|
100
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") console.warn(formatFactoryLazyWarning(target));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve a constructor, managing singleton lifetimes and detecting circular dependencies.
|
|
104
|
+
* This is the core resolution logic that orchestrates caching, coalescing, and instantiation.
|
|
105
|
+
*
|
|
106
|
+
* @param target - Service constructor to resolve
|
|
107
|
+
* @param resolutionStack - Chain of services currently being resolved (for cycle detection)
|
|
108
|
+
* @returns Promise resolving to the service instance
|
|
109
|
+
* @throws Error if a circular dependency is detected
|
|
110
|
+
*/
|
|
111
|
+
async resolve(target, resolutionStack) {
|
|
112
|
+
const overridden = this.instanceOverrides.get(target);
|
|
113
|
+
if (overridden) return overridden;
|
|
114
|
+
if (resolutionStack.includes(target)) throw new DependencyResolutionError(`Circular dependency detected: ${[...resolutionStack.map((t) => t.name), target.name].join(" -> ")}`, {
|
|
115
|
+
target,
|
|
116
|
+
resolutionStack,
|
|
117
|
+
failedDependency: target
|
|
118
|
+
});
|
|
119
|
+
const metadata = this.getServiceMetadata(target);
|
|
120
|
+
const nextStack = [...resolutionStack, target];
|
|
121
|
+
if (metadata.scope === ServiceScope.SINGLETON) return this.resolveSingleton(target, metadata, nextStack);
|
|
122
|
+
return this.createInstance(target, metadata.dependencies, nextStack, metadata.factory);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a singleton service with caching and in-flight creation coalescing.
|
|
126
|
+
*/
|
|
127
|
+
async resolveSingleton(target, metadata, resolutionStack) {
|
|
128
|
+
const cached = this.singletons.get(target);
|
|
129
|
+
if (cached) return cached;
|
|
130
|
+
const pending = this.pendingSingletons.get(target);
|
|
131
|
+
if (pending) return await pending;
|
|
132
|
+
const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory).then((instance) => {
|
|
133
|
+
this.singletons.set(target, instance);
|
|
134
|
+
return instance;
|
|
135
|
+
});
|
|
136
|
+
this.pendingSingletons.set(target, creation);
|
|
137
|
+
try {
|
|
138
|
+
return await creation;
|
|
139
|
+
} finally {
|
|
140
|
+
this.pendingSingletons.delete(target);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Instantiate a class by resolving and injecting all declared dependencies.
|
|
145
|
+
* Handles factory-lazy services by importing the real class before instantiation.
|
|
146
|
+
*
|
|
147
|
+
* @param target - Service constructor (may be a stub class if factory provided)
|
|
148
|
+
* @param dependencies - Array of dependency items to resolve and inject
|
|
149
|
+
* @param resolutionStack - Current resolution chain
|
|
150
|
+
* @param factory - Optional lazy factory to import the real class
|
|
151
|
+
* @returns Promise resolving to the instantiated service
|
|
152
|
+
*/
|
|
153
|
+
async createInstance(target, dependencies, resolutionStack, factory) {
|
|
154
|
+
const ctor = factory ? await this.importWithRetry(factory, target, resolutionStack) : target;
|
|
155
|
+
return new ctor(...await Promise.all(dependencies.map((param) => this.resolveParam(param, ctor, resolutionStack))));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Resolve a single dependency entry, handling lazies, tokens, and constructors.
|
|
159
|
+
* This is called for each parameter in a service's dependency array.
|
|
160
|
+
*
|
|
161
|
+
* @param param - Dependency item (can be Lazy, Token, or Constructor)
|
|
162
|
+
* @param target - Service being constructed (for error messages)
|
|
163
|
+
* @param resolutionStack - Current resolution chain
|
|
164
|
+
* @returns Promise resolving to the dependency instance
|
|
165
|
+
* @throws Error if dependency type is invalid
|
|
166
|
+
*/
|
|
167
|
+
async resolveParam(param, target, resolutionStack) {
|
|
168
|
+
const classification = classifyDependency(param);
|
|
169
|
+
if (!classification) {
|
|
170
|
+
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
171
|
+
throw new DependencyResolutionError(`Invalid dependency type while resolving ${target.name}. Resolution stack: ${stackPath}. Received type: ${typeof param}`, {
|
|
172
|
+
target,
|
|
173
|
+
resolutionStack,
|
|
174
|
+
failedDependency: param
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
switch (classification.kind) {
|
|
178
|
+
case "lazy": {
|
|
179
|
+
const depClass = await this.importWithRetry(classification.lazy, target, resolutionStack);
|
|
180
|
+
return this.resolve(depClass, resolutionStack);
|
|
181
|
+
}
|
|
182
|
+
case "token": return this.resolveTokenLike(classification.token, target, resolutionStack);
|
|
183
|
+
case "constructor": return this.resolve(classification.ctor, resolutionStack);
|
|
184
|
+
}
|
|
185
|
+
return classification;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Execute a lazy importer with optional retry/backoff semantics.
|
|
189
|
+
* Implements exponential backoff for transient network failures.
|
|
190
|
+
*
|
|
191
|
+
* @param lazyDep - Lazy dependency wrapper with importer function and retry config
|
|
192
|
+
* @param target - Service being resolved (for error messages)
|
|
193
|
+
* @param resolutionStack - Current resolution chain (for cycle detection and error context)
|
|
194
|
+
* @returns The imported class constructor
|
|
195
|
+
* @throws Error if all retry attempts exhausted or import returns non-constructor
|
|
196
|
+
*/
|
|
197
|
+
async importWithRetry(lazyDep, target, resolutionStack) {
|
|
198
|
+
const runImport = async () => await lazyDep.importer();
|
|
199
|
+
const retries = lazyDep.retry?.retries ?? 0;
|
|
200
|
+
const baseDelay = lazyDep.retry?.backoffMs ?? 0;
|
|
201
|
+
const factor = lazyDep.retry?.factor ?? 2;
|
|
202
|
+
let attempt = 0;
|
|
203
|
+
while (true) try {
|
|
204
|
+
const module = await runImport();
|
|
205
|
+
const depClass = typeof module === "object" && module !== null && "default" in module ? module.default : module;
|
|
206
|
+
if (!isConstructor(depClass)) {
|
|
207
|
+
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
208
|
+
throw new DependencyResolutionError(`Lazy importer did not return a class for dependency while resolving ${target.name}. Resolution stack: ${stackPath}. Received type: ${typeof depClass}`, {
|
|
209
|
+
target,
|
|
210
|
+
resolutionStack,
|
|
211
|
+
failedDependency: depClass
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return depClass;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (attempt >= retries) {
|
|
217
|
+
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
218
|
+
throw new DependencyResolutionError(`Failed to import lazy dependency while resolving ${target.name}. Resolution stack: ${stackPath}. Original error: ${err instanceof Error ? err.message : String(err)}`, {
|
|
219
|
+
target,
|
|
220
|
+
resolutionStack,
|
|
221
|
+
failedDependency: lazyDep,
|
|
222
|
+
cause: err
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const delay = baseDelay * Math.pow(factor, attempt);
|
|
226
|
+
if (delay > 0) await new Promise((r) => setTimeout(r, delay));
|
|
227
|
+
attempt++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Resolve a token dependency via registered value providers.
|
|
232
|
+
*/
|
|
233
|
+
resolveTokenLike(tok, target, resolutionStack) {
|
|
234
|
+
if (this.valueProviders.has(tok.id)) return this.valueProviders.get(tok.id);
|
|
235
|
+
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
236
|
+
throw new DependencyResolutionError(`No provider registered for token ${tok.description ?? String(tok.id)} while resolving ${target.name}. Resolution stack: ${stackPath}`, {
|
|
237
|
+
target,
|
|
238
|
+
resolutionStack,
|
|
239
|
+
failedDependency: tok
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Format a readable representation of the resolution stack for error messages.
|
|
244
|
+
*/
|
|
245
|
+
formatStackPath(target, resolutionStack) {
|
|
246
|
+
return [...resolutionStack.map((t) => t.name), target.name].join(" -> ");
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Retrieve (and memoize) the DI metadata for a service from the registry.
|
|
250
|
+
*/
|
|
251
|
+
getServiceMetadata(target) {
|
|
252
|
+
const cached = this.metadataCache.get(target);
|
|
253
|
+
if (cached) return cached;
|
|
254
|
+
const registryEntry = dependenciesRegistry.get(target);
|
|
255
|
+
const metadata = {
|
|
256
|
+
scope: registryEntry?.scope ?? ServiceScope.TRANSIENT,
|
|
257
|
+
dependencies: (registryEntry?.dependencies ?? (() => []))(),
|
|
258
|
+
factory: registryEntry?.factory
|
|
259
|
+
};
|
|
260
|
+
this.metadataCache.set(target, metadata);
|
|
261
|
+
return metadata;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
export { Container };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Newable, Token } from "./types.js";
|
|
2
|
+
import { Lazy } from "./lazy.js";
|
|
3
|
+
import { ServiceScope } from "./scope.js";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/decorators.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a declared dependency to the constructor parameter type expected by the service.
|
|
9
|
+
*
|
|
10
|
+
* - If the dependency is a `Lazy<T>`, this resolves to `T` (the eagerly-constructed type).
|
|
11
|
+
* - If the dependency is a `Newable<T>` (a class constructor), this resolves to `T`.
|
|
12
|
+
* - If the dependency is a `Token<T>`, this resolves to the provided value type `T`.
|
|
13
|
+
* - Otherwise, resolves to `never`.
|
|
14
|
+
*
|
|
15
|
+
* This is used to map the `dependencies` tuple into the service constructor parameter list.
|
|
16
|
+
*
|
|
17
|
+
* @typeParam D - A single declared dependency item.
|
|
18
|
+
*/
|
|
19
|
+
type ResolveDep<D> = D extends Lazy<infer L> ? L : D extends Newable<infer I> ? I : D extends Token<infer V> ? V : never;
|
|
20
|
+
/**
|
|
21
|
+
* Tuple-map a declared dependencies list to the constructor parameter types.
|
|
22
|
+
*
|
|
23
|
+
* Given a tuple of constructors and/or `Lazy<...>` wrappers, produces a tuple of the
|
|
24
|
+
* instance types the class constructor should accept.
|
|
25
|
+
*
|
|
26
|
+
* Example:
|
|
27
|
+
* - `[Logger, Metrics]` -> `[Logger, Metrics]` (instances)
|
|
28
|
+
* - `[Lazy(() => Logger)]` -> `[Logger]`
|
|
29
|
+
*
|
|
30
|
+
* @typeParam TDeps - A readonly tuple of declared dependencies.
|
|
31
|
+
*/
|
|
32
|
+
type DepInstances<TDeps extends readonly unknown[]> = { [K in keyof TDeps]: ResolveDep<TDeps[K]> };
|
|
33
|
+
/**
|
|
34
|
+
* Allowed shapes for the `dependencies` option.
|
|
35
|
+
*
|
|
36
|
+
* - A readonly tuple/array of constructors and/or `Lazy<...>` wrappers.
|
|
37
|
+
* - Or a function returning such a tuple (recommended to break circular refs in the same file).
|
|
38
|
+
*/
|
|
39
|
+
type DependencyItem = Newable<unknown> | Lazy<unknown> | Token<unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* Strongly-typed decorator function signature used by overloads when dependencies are known.
|
|
42
|
+
*
|
|
43
|
+
* @typeParam TDeps - A readonly tuple of declared dependencies.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
type TypedClassDecorator<TDeps extends readonly DependencyItem[]> = (target: new (...args: DepInstances<TDeps>) => unknown) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Global registry for service metadata used by the runtime container.
|
|
49
|
+
*
|
|
50
|
+
* Keys are service constructors; values include the configured scope and a
|
|
51
|
+
* normalized dependency function that returns the declared dependencies as a readonly tuple.
|
|
52
|
+
*
|
|
53
|
+
* The Vite plugin populates this from source decorators at build time, but you can also
|
|
54
|
+
* register programmatically via the decorators in tests or non-plugin setups.
|
|
55
|
+
*
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
declare const dependenciesRegistry: Map<Newable<unknown>, {
|
|
59
|
+
dependencies?: () => readonly (Newable<unknown> | Lazy<unknown> | Token<unknown>)[];
|
|
60
|
+
scope?: ServiceScope;
|
|
61
|
+
factory?: Lazy<Newable<unknown>>;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Class decorator for declaring a DI-managed service and its dependencies.
|
|
65
|
+
*
|
|
66
|
+
* Overloads preserve tuple inference for both array and function dependency forms, enabling
|
|
67
|
+
* strict alignment between the declared dependencies and the class constructor parameter list.
|
|
68
|
+
*
|
|
69
|
+
* Notes:
|
|
70
|
+
* - Order matters: dependencies map positionally to constructor parameters.
|
|
71
|
+
* - For circular dependencies in the same file, use the function form: `@Injectable(() => [B])`.
|
|
72
|
+
* - When using `Lazy(...)`, the constructor expects the resolved type, not `Lazy<T>`.
|
|
73
|
+
* - Due to TypeScript limitations, decorator position may not always surface target mismatches;
|
|
74
|
+
* use {@link assertDeps} for zero-cost compile-time assertions when needed.
|
|
75
|
+
*
|
|
76
|
+
* @typeParam TDeps - A readonly tuple of declared dependencies.
|
|
77
|
+
* @param depsOrScope - Optional dependency declaration followed by an optional scope string.
|
|
78
|
+
*
|
|
79
|
+
* @example Transient with a direct dependency
|
|
80
|
+
* ```ts
|
|
81
|
+
* @Injectable(deps(Logger))
|
|
82
|
+
* class AppService {
|
|
83
|
+
* constructor(private logger: Logger) {}
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @example Singleton shorthand via scope
|
|
88
|
+
* ```ts
|
|
89
|
+
* @Injectable('singleton')
|
|
90
|
+
* class AppState {}
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @example Circular dependency in the same file
|
|
94
|
+
* ```ts
|
|
95
|
+
* @Injectable(() => [CircularB])
|
|
96
|
+
* class CircularA { constructor(private b: CircularB) {} }
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example Lazy dependency resolves to the underlying type
|
|
100
|
+
* ```ts
|
|
101
|
+
* @Injectable([Lazy(() => Promise.resolve(Logger))])
|
|
102
|
+
* class NeedsLogger { constructor(private logger: Logger) {} }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
declare function Injectable(): ClassDecorator;
|
|
106
|
+
declare function Injectable(scope: ServiceScope): ClassDecorator;
|
|
107
|
+
declare function Injectable<const TDeps extends readonly DependencyItem[]>(dependencies: () => TDeps, scope?: ServiceScope): TypedClassDecorator<TDeps>;
|
|
108
|
+
declare function Injectable<const TDeps extends readonly DependencyItem[]>(dependencies: TDeps, scope?: ServiceScope): TypedClassDecorator<TDeps>;
|
|
109
|
+
/**
|
|
110
|
+
* Shorthand decorator for singleton services.
|
|
111
|
+
*
|
|
112
|
+
* Equivalent to `@Injectable(dependencies?, 'singleton')` with the same strict typing
|
|
113
|
+
* and overload behaviors as {@link Injectable}.
|
|
114
|
+
*
|
|
115
|
+
* @typeParam TDeps - A readonly tuple of declared dependencies.
|
|
116
|
+
* @param options - Singleton configuration and dependencies (array or function form).
|
|
117
|
+
*
|
|
118
|
+
* @example No dependencies
|
|
119
|
+
* ```ts
|
|
120
|
+
* @Singleton()
|
|
121
|
+
* class GlobalLogger {}
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @example With dependencies (array form)
|
|
125
|
+
* ```ts
|
|
126
|
+
* @Singleton(deps(Config))
|
|
127
|
+
* class Metrics { constructor(private cfg: Config) {} }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
declare function Singleton(): ClassDecorator;
|
|
131
|
+
declare function Singleton<const TDeps extends readonly DependencyItem[]>(dependencies: () => TDeps): TypedClassDecorator<TDeps>;
|
|
132
|
+
declare function Singleton<const TDeps extends readonly DependencyItem[]>(dependencies: TDeps): TypedClassDecorator<TDeps>;
|
|
133
|
+
/**
|
|
134
|
+
* Declare dependencies as a strongly-typed readonly tuple without `as const`.
|
|
135
|
+
*
|
|
136
|
+
* This helper preserves tuple inference for strict constructor checking while keeping callsites
|
|
137
|
+
* concise. It returns a function so it can be used directly as `dependencies` in the decorator.
|
|
138
|
+
*
|
|
139
|
+
* @typeParam T - A readonly tuple of constructors and/or `Lazy<...>` wrappers.
|
|
140
|
+
* @param items - Variadic dependency list.
|
|
141
|
+
* @returns A function returning the same tuple (suitable for `dependencies`).
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* @Injectable(deps(Logger, Metrics))
|
|
146
|
+
* class AppService { constructor(l: Logger, m: Metrics) {} }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
declare function deps<T extends readonly (Newable<unknown> | Lazy<unknown> | Token<unknown>)[]>(...items: T): () => T;
|
|
150
|
+
/**
|
|
151
|
+
* Compile-time assertion: ensure constructor parameters match the resolved dependency tuple.
|
|
152
|
+
*
|
|
153
|
+
* This function has zero runtime cost and simply returns the class unchanged. It exists solely
|
|
154
|
+
* to force a TypeScript evaluation that surfaces mismatches (number, order, or type), including
|
|
155
|
+
* cases where decorator position alone may not report errors due to compiler limitations.
|
|
156
|
+
*
|
|
157
|
+
* @typeParam TDeps - Declared dependency tuple.
|
|
158
|
+
* @typeParam TClass - Class type with a constructor that must accept `DepInstances<TDeps>`.
|
|
159
|
+
* @param depsFn - A function returning the declared dependency tuple (e.g., from {@link deps}).
|
|
160
|
+
* @param klass - The class constructor to validate.
|
|
161
|
+
* @returns The same class constructor, unchanged.
|
|
162
|
+
*
|
|
163
|
+
* @example Negative test in code
|
|
164
|
+
* ```ts
|
|
165
|
+
* class Dep {}
|
|
166
|
+
* class Wrong {}
|
|
167
|
+
*
|
|
168
|
+
* @Injectable(deps(Dep))
|
|
169
|
+
* class UsesWrong { constructor(_: Wrong) {} }
|
|
170
|
+
*
|
|
171
|
+
* // @ts-expect-error mismatch detected at compile time
|
|
172
|
+
* assertDeps(deps(Dep), UsesWrong);
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
declare function assertDeps<TDeps extends readonly (Newable<unknown> | Lazy<unknown> | Token<unknown>)[], TClass extends new (...args: DepInstances<TDeps>) => unknown>(depsFn: () => TDeps, klass: TClass): TClass;
|
|
176
|
+
//#endregion
|
|
177
|
+
export { Injectable, Singleton, assertDeps, dependenciesRegistry, deps };
|