feat: init project
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
22
.zed/settings.json
Normal file
22
.zed/settings.json
Normal file
@@ -0,0 +1,22 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{
|
||||
"formatter": { "language_server": { "name": "biome" } },
|
||||
"code_actions_on_format": {
|
||||
"source.fixAll.biome": true,
|
||||
"source.organizeImports.biome": true
|
||||
},
|
||||
"lsp": {
|
||||
"biome": {
|
||||
"settings": {
|
||||
"require_config_file": true
|
||||
},
|
||||
"binary": {
|
||||
"path": "node_modules/.bin/biome",
|
||||
"arguments": ["lsp-proxy"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
README.md
10
README.md
@@ -1,4 +1,6 @@
|
||||
# core
|
||||
# Alveo
|
||||
|
||||
Alveo is a lightweight example of DI (Dependency Injection) container for TypeScript and JavaScript projects, designed to be simple and easy to use.
|
||||
|
||||
To install dependencies:
|
||||
|
||||
@@ -6,10 +8,8 @@ To install dependencies:
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
To run test:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
bun test.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
|
||||
68
biome.json
Normal file
68
biome.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 4,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"noStaticOnlyClass": "off",
|
||||
"noBannedTypes": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "error",
|
||||
"noUnusedPrivateClassMembers": "error",
|
||||
"noUnusedFunctionParameters": "off"
|
||||
},
|
||||
"style": {
|
||||
"useImportType": "off",
|
||||
"useNodejsImportProtocol": "off",
|
||||
"useTemplate": "off",
|
||||
"noNonNullAssertion": "off",
|
||||
"useLiteralEnumMembers": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noImplicitAnyLet": "off",
|
||||
"noAssignInExpressions": "off",
|
||||
"useIterableCallbackReturn": "off",
|
||||
"noShadowRestrictedNames": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
},
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["dist/**", "node_modules/**"],
|
||||
"linter": {
|
||||
"enabled": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
50
bun.lock
Normal file
50
bun.lock
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "core",
|
||||
"dependencies": {
|
||||
"reflect-metadata": "^0.2.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||
|
||||
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
}
|
||||
}
|
||||
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@alveojs/core",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "biome format",
|
||||
"lint": "biome lint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
339
src/container/Container.ts
Normal file
339
src/container/Container.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import "reflect-metadata";
|
||||
import { CircularDependencyError, ProviderNotFoundError } from "../errors";
|
||||
import { ContextualModuleRef, ModuleRef } from "../module/ModuleRef";
|
||||
import type { Constructor } from "../types";
|
||||
import {
|
||||
type BaseProvider,
|
||||
INJECT_METADATA_KEY,
|
||||
INJECTABLE_METADATA_KEY,
|
||||
MODULE_METADATA_KEY,
|
||||
type ModuleOptions,
|
||||
type Provider,
|
||||
type ProviderToken,
|
||||
type ResolvedProvider,
|
||||
} from "./types";
|
||||
|
||||
interface ParamType {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
type LifecycleHook = "onModuleInit" | "onModuleDestroy";
|
||||
type LifecycleHookFn = () => Promise<void> | void;
|
||||
type LifecycleAware = Partial<Record<LifecycleHook, LifecycleHookFn>>;
|
||||
|
||||
export class Container {
|
||||
private readonly providers = new Map<
|
||||
string,
|
||||
Map<ProviderToken, ResolvedProvider>
|
||||
>();
|
||||
private readonly instances = new Map<string, Map<ProviderToken, unknown>>();
|
||||
private readonly moduleOptions = new Map<string, ModuleOptions>();
|
||||
private readonly resolving = new Set<ProviderToken>();
|
||||
private rootModuleName?: string;
|
||||
|
||||
constructor(private readonly globalContext = "global") {
|
||||
this.ensureScope(this.globalContext);
|
||||
}
|
||||
|
||||
public registerRootModule(moduleClass: Constructor): void {
|
||||
this.rootModuleName = moduleClass.name;
|
||||
this.registerModule(moduleClass);
|
||||
}
|
||||
|
||||
public getRootModuleName(): string | undefined {
|
||||
return this.rootModuleName;
|
||||
}
|
||||
|
||||
public registerModule(moduleClass: Constructor): void {
|
||||
const options: ModuleOptions | undefined = Reflect.getMetadata(
|
||||
MODULE_METADATA_KEY,
|
||||
moduleClass,
|
||||
);
|
||||
if (!options) return;
|
||||
|
||||
const moduleName = moduleClass.name;
|
||||
if (this.moduleOptions.has(moduleName)) return;
|
||||
|
||||
this.moduleOptions.set(moduleName, options);
|
||||
this.ensureScope(moduleName);
|
||||
|
||||
if (options.imports) {
|
||||
for (const imported of options.imports) {
|
||||
this.registerModule(imported);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.providers) {
|
||||
for (const provider of options.providers) {
|
||||
const normalized = this.normalizeProvider(provider, moduleName);
|
||||
this.providers
|
||||
.get(moduleName)!
|
||||
.set(normalized.token, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
const moduleProvider = this.normalizeProvider(moduleClass, moduleName);
|
||||
this.providers.get(moduleName)!.set(moduleClass, moduleProvider);
|
||||
}
|
||||
|
||||
public registerProvider(provider: Provider): void {
|
||||
const normalized = this.normalizeProvider(provider, this.globalContext);
|
||||
this.providers
|
||||
.get(this.globalContext)!
|
||||
.set(normalized.token, normalized);
|
||||
}
|
||||
|
||||
public async get<T>(
|
||||
token: ProviderToken<T>,
|
||||
moduleName?: string,
|
||||
): Promise<T> {
|
||||
return this.resolve(token, moduleName);
|
||||
}
|
||||
|
||||
public async resolveAll(): Promise<void> {
|
||||
for (const [scope] of this.providers) {
|
||||
for (const token of this.providers.get(scope)!.keys()) {
|
||||
await this.resolve(
|
||||
token,
|
||||
scope === this.globalContext ? undefined : scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async callLifecycleHook(
|
||||
hook: "onModuleInit" | "onModuleDestroy",
|
||||
): Promise<void> {
|
||||
for (const instances of this.instances.values()) {
|
||||
for (const instance of instances.values()) {
|
||||
if (!instance) continue;
|
||||
const lifecycleInstance = instance as LifecycleAware;
|
||||
const hookFn = lifecycleInstance[hook];
|
||||
if (typeof hookFn === "function") {
|
||||
await hookFn.call(lifecycleInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async resolve<T>(
|
||||
token: ProviderToken<T>,
|
||||
moduleName?: string,
|
||||
): Promise<T> {
|
||||
const context = moduleName ?? this.globalContext;
|
||||
|
||||
if (token === ModuleRef) {
|
||||
return new ContextualModuleRef(this, context) as T;
|
||||
}
|
||||
|
||||
let provider = this.findProvider(token, context);
|
||||
|
||||
if (
|
||||
!provider &&
|
||||
context === this.globalContext &&
|
||||
this.rootModuleName
|
||||
) {
|
||||
provider = this.findProvider(token, this.rootModuleName);
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new ProviderNotFoundError(
|
||||
typeof token === "function" ? token.name : String(token),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.scope === "singleton") {
|
||||
const existing = this.instances
|
||||
.get(provider.moduleName ?? context)
|
||||
?.get(provider.token);
|
||||
if (existing !== undefined) {
|
||||
return existing as T;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.resolving.has(token)) {
|
||||
const stack = [...this.resolving].map((item) =>
|
||||
typeof item === "function" ? item.name : String(item),
|
||||
);
|
||||
stack.push(
|
||||
typeof token === "function" ? token.name : String(token),
|
||||
);
|
||||
throw new CircularDependencyError(stack);
|
||||
}
|
||||
|
||||
this.resolving.add(token);
|
||||
|
||||
try {
|
||||
let instance: T;
|
||||
|
||||
if (provider.useValue !== undefined) {
|
||||
instance = provider.useValue as T;
|
||||
} else if (provider.useFactory) {
|
||||
const dependencies = await Promise.all(
|
||||
(provider.inject ?? []).map((dep) =>
|
||||
this.resolve(dep, provider.moduleName ?? context),
|
||||
),
|
||||
);
|
||||
instance = (await provider.useFactory(...dependencies)) as T;
|
||||
} else if (provider.useExisting) {
|
||||
instance = await this.resolve<T>(
|
||||
provider.useExisting as ProviderToken<T>,
|
||||
provider.moduleName ?? context,
|
||||
);
|
||||
} else if (provider.useClass) {
|
||||
const targetClass = provider.useClass;
|
||||
const paramTypes: ParamType[] =
|
||||
Reflect.getMetadata("design:paramtypes", targetClass) || [];
|
||||
const injectTokens: ProviderToken[] =
|
||||
Reflect.getMetadata(INJECT_METADATA_KEY, targetClass) || [];
|
||||
|
||||
this.debug(`[Container] Instantiating ${targetClass.name}`);
|
||||
this.debug(
|
||||
"[Container] ParamTypes:",
|
||||
paramTypes.map((p) => p?.name || String(p)),
|
||||
);
|
||||
this.debug(
|
||||
"[Container] InjectTokens:",
|
||||
injectTokens.map((token) =>
|
||||
token ? String(token) : "undefined",
|
||||
),
|
||||
);
|
||||
|
||||
const dependencies = await Promise.all(
|
||||
paramTypes.map(async (paramType, index) => {
|
||||
const depToken =
|
||||
injectTokens[index] || (paramType as ProviderToken);
|
||||
this.debug(
|
||||
`[Container] Resolving dependency ${index}: ${String(depToken)}`,
|
||||
);
|
||||
return this.resolve(
|
||||
depToken as ProviderToken<unknown>,
|
||||
provider.moduleName ?? context,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
instance = new targetClass(...dependencies) as T;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid provider configuration for token ${String(token)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.scope === "singleton") {
|
||||
const scopeName = provider.moduleName ?? context;
|
||||
this.ensureScope(scopeName);
|
||||
this.instances.get(scopeName)!.set(provider.token, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
} finally {
|
||||
this.resolving.delete(token);
|
||||
}
|
||||
}
|
||||
|
||||
private findProvider(
|
||||
token: ProviderToken,
|
||||
context: string,
|
||||
): ResolvedProvider | undefined {
|
||||
const moduleProviders = this.providers.get(context);
|
||||
if (moduleProviders?.has(token)) {
|
||||
return moduleProviders.get(token);
|
||||
}
|
||||
|
||||
const options = this.moduleOptions.get(context);
|
||||
if (options?.imports) {
|
||||
for (const imported of options.imports) {
|
||||
const importedProviders = this.providers.get(imported.name);
|
||||
const importedOptions = this.moduleOptions.get(imported.name);
|
||||
if (
|
||||
importedProviders?.has(token) &&
|
||||
importedOptions?.exports?.includes(token)
|
||||
) {
|
||||
return importedProviders.get(token);
|
||||
}
|
||||
if (token === imported) {
|
||||
return importedProviders?.get(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context !== this.globalContext) {
|
||||
const globalProviders = this.providers.get(this.globalContext);
|
||||
if (globalProviders?.has(token)) {
|
||||
return globalProviders.get(token);
|
||||
}
|
||||
}
|
||||
|
||||
if (context === this.globalContext && this.rootModuleName) {
|
||||
const rootProviders = this.providers.get(this.rootModuleName);
|
||||
if (rootProviders?.has(token)) {
|
||||
return rootProviders.get(token);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private normalizeProvider(
|
||||
provider: Provider,
|
||||
moduleName?: string,
|
||||
): ResolvedProvider {
|
||||
if (typeof provider === "function") {
|
||||
const metadata = Reflect.getMetadata(
|
||||
INJECTABLE_METADATA_KEY,
|
||||
provider,
|
||||
);
|
||||
return {
|
||||
token: provider,
|
||||
scope: metadata?.scope ?? "singleton",
|
||||
useClass: provider,
|
||||
moduleName,
|
||||
};
|
||||
}
|
||||
|
||||
const base = {
|
||||
token: provider.provide,
|
||||
scope: provider.scope ?? "singleton",
|
||||
moduleName,
|
||||
};
|
||||
|
||||
if ("useClass" in provider) {
|
||||
return { ...base, useClass: provider.useClass };
|
||||
}
|
||||
if ("useValue" in provider) {
|
||||
return { ...base, useValue: provider.useValue };
|
||||
}
|
||||
if ("useFactory" in provider) {
|
||||
return {
|
||||
...base,
|
||||
useFactory: provider.useFactory,
|
||||
inject: provider.inject,
|
||||
};
|
||||
}
|
||||
if ("useExisting" in provider) {
|
||||
return { ...base, useExisting: provider.useExisting };
|
||||
}
|
||||
|
||||
const exhaustiveCheck: never = provider;
|
||||
throw new Error(
|
||||
`Invalid provider definition for token ${String((exhaustiveCheck as BaseProvider).provide)}`,
|
||||
);
|
||||
}
|
||||
|
||||
private ensureScope(name: string): void {
|
||||
if (!this.providers.has(name)) {
|
||||
this.providers.set(name, new Map());
|
||||
}
|
||||
if (!this.instances.has(name)) {
|
||||
this.instances.set(name, new Map());
|
||||
}
|
||||
}
|
||||
|
||||
private debug(...args: unknown[]): void {
|
||||
if (process.env.ALVEO_CONTAINER_DEBUG === "true") {
|
||||
console.debug(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/container/types.ts
Normal file
60
src/container/types.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { Constructor, Type } from "../types";
|
||||
|
||||
export type ProviderScope = "singleton" | "transient";
|
||||
|
||||
export type ProviderToken<T = unknown> = Type<T> | string | symbol;
|
||||
|
||||
export interface BaseProvider<T = unknown> {
|
||||
provide: ProviderToken<T>;
|
||||
scope?: ProviderScope;
|
||||
}
|
||||
|
||||
export interface ClassProvider<T = unknown> extends BaseProvider<T> {
|
||||
useClass: Constructor<T>;
|
||||
}
|
||||
|
||||
export interface ValueProvider<T = unknown> extends BaseProvider<T> {
|
||||
useValue: T;
|
||||
}
|
||||
|
||||
export interface FactoryProvider<T = unknown> extends BaseProvider<T> {
|
||||
useFactory: (...args: unknown[]) => T | Promise<T>;
|
||||
inject?: ProviderToken[];
|
||||
}
|
||||
|
||||
export interface ExistingProvider<T = unknown> extends BaseProvider<T> {
|
||||
useExisting: ProviderToken<T>;
|
||||
}
|
||||
|
||||
export type Provider<T = unknown> =
|
||||
| Constructor<T>
|
||||
| ClassProvider<T>
|
||||
| ValueProvider<T>
|
||||
| FactoryProvider<T>
|
||||
| ExistingProvider<T>;
|
||||
|
||||
export interface ResolvedProvider<T = unknown> {
|
||||
token: ProviderToken<T>;
|
||||
scope: ProviderScope;
|
||||
useClass?: Constructor<T>;
|
||||
useValue?: T;
|
||||
useFactory?: (...args: unknown[]) => T | Promise<T>;
|
||||
useExisting?: ProviderToken<T>;
|
||||
inject?: ProviderToken[];
|
||||
moduleName?: string;
|
||||
}
|
||||
|
||||
export interface InjectableOptions {
|
||||
scope?: ProviderScope;
|
||||
}
|
||||
|
||||
export interface ModuleOptions {
|
||||
imports?: Constructor[];
|
||||
providers?: Provider[];
|
||||
exports?: ProviderToken[];
|
||||
}
|
||||
|
||||
export const INJECTABLE_METADATA_KEY = "alveo:injectable";
|
||||
export const INJECT_METADATA_KEY = "alveo:inject";
|
||||
export const MODULE_METADATA_KEY = "alveo:module";
|
||||
export const PARAMTYPES_METADATA_KEY = "design:paramtypes";
|
||||
3
src/decorators/index.ts
Normal file
3
src/decorators/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./inject.decorator";
|
||||
export * from "./injectable.decorator";
|
||||
export * from "./module.decorator";
|
||||
22
src/decorators/inject.decorator.ts
Normal file
22
src/decorators/inject.decorator.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import "reflect-metadata";
|
||||
import { INJECT_METADATA_KEY, type ProviderToken } from "../container/types";
|
||||
|
||||
/**
|
||||
* Decorator that explicitly specifies a token for dependency injection.
|
||||
* Useful when injecting interfaces (via symbols or strings) or when multiple
|
||||
* providers are available for the same type.
|
||||
*
|
||||
* @param token The token to inject
|
||||
*/
|
||||
export function Inject(token: ProviderToken): ParameterDecorator {
|
||||
return (
|
||||
target: object,
|
||||
_propertyKey: string | symbol | undefined,
|
||||
parameterIndex: number,
|
||||
) => {
|
||||
const existingInjections =
|
||||
Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
|
||||
existingInjections[parameterIndex] = token;
|
||||
Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target);
|
||||
};
|
||||
}
|
||||
16
src/decorators/injectable.decorator.ts
Normal file
16
src/decorators/injectable.decorator.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import "reflect-metadata";
|
||||
import {
|
||||
INJECTABLE_METADATA_KEY,
|
||||
type InjectableOptions,
|
||||
} from "../container/types";
|
||||
|
||||
/**
|
||||
* Decorator that marks a class as available to be provided and injected as a dependency.
|
||||
*
|
||||
* @param options Options for the injectable (e.g., scope)
|
||||
*/
|
||||
export function Injectable(options?: InjectableOptions): ClassDecorator {
|
||||
return (target: object) => {
|
||||
Reflect.defineMetadata(INJECTABLE_METADATA_KEY, options || {}, target);
|
||||
};
|
||||
}
|
||||
14
src/decorators/module.decorator.ts
Normal file
14
src/decorators/module.decorator.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import "reflect-metadata";
|
||||
import { MODULE_METADATA_KEY, type ModuleOptions } from "../container/types";
|
||||
|
||||
/**
|
||||
* Decorator that marks a class as an Alveo module.
|
||||
* Modules are used to organize the application structure and define providers, imports, and exports.
|
||||
*
|
||||
* @param options Module configuration options
|
||||
*/
|
||||
export function Module(options: ModuleOptions): ClassDecorator {
|
||||
return (target: object) => {
|
||||
Reflect.defineMetadata(MODULE_METADATA_KEY, options, target);
|
||||
};
|
||||
}
|
||||
47
src/errors/index.ts
Normal file
47
src/errors/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export class AlveoError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProviderNotFoundError extends AlveoError {
|
||||
constructor(token: string, context?: string) {
|
||||
const contextMsg = context ? ` in the "${context}" context` : "";
|
||||
super(
|
||||
`Alveo can't resolve dependencies of the ${token}${contextMsg}. Please make sure that it is available in the current module scope.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CircularDependencyError extends AlveoError {
|
||||
constructor(stack: string[]) {
|
||||
super(`Circular dependency detected: ${stack.join(" -> ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidModuleError extends AlveoError {
|
||||
constructor(target: string) {
|
||||
super(
|
||||
`The class "${target}" is not a valid Alveo module. Did you forget the @Module() decorator?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidProviderError extends AlveoError {
|
||||
constructor(provider: unknown) {
|
||||
super(`Invalid provider definition: ${JSON.stringify(provider)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class LifecycleError extends AlveoError {
|
||||
constructor(hook: string, error: Error) {
|
||||
super(`Error during lifecycle hook "${hook}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class BootstrapError extends AlveoError {
|
||||
constructor(message: string) {
|
||||
super(`Application bootstrap failed: ${message}`);
|
||||
}
|
||||
}
|
||||
36
src/factory/AlveoApplication.ts
Normal file
36
src/factory/AlveoApplication.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Container } from "../container/Container";
|
||||
import type { ProviderToken } from "../container/types";
|
||||
|
||||
/**
|
||||
* AlveoApplication is the main class representing an Alveo application instance.
|
||||
* It provides methods to interact with the dependency injection container and manage the application lifecycle.
|
||||
*/
|
||||
export class AlveoApplication {
|
||||
constructor(private readonly container: Container) {}
|
||||
|
||||
/**
|
||||
* Retrieves an instance of a provider from the application container.
|
||||
*
|
||||
* @param token The provider token (Class, String, or Symbol)
|
||||
* @returns A promise resolving to the instance of the provider
|
||||
*/
|
||||
public async get<T>(token: ProviderToken<T>): Promise<T> {
|
||||
return this.container.get(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the application by calling onModuleInit hooks on all providers.
|
||||
* This is called automatically by AlveoFactory.create().
|
||||
*/
|
||||
public async init(): Promise<this> {
|
||||
await this.container.callLifecycleHook("onModuleInit");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully shuts down the application by calling onModuleDestroy hooks.
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
await this.container.callLifecycleHook("onModuleDestroy");
|
||||
}
|
||||
}
|
||||
38
src/factory/AlveoFactory.ts
Normal file
38
src/factory/AlveoFactory.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Container } from "../container/Container";
|
||||
import type { Constructor } from "../types";
|
||||
import { AlveoApplication } from "./AlveoApplication";
|
||||
|
||||
/**
|
||||
* AlveoFactory is the entry point for creating Alveo application instances.
|
||||
* It handles the creation of the dependency injection container and module registration.
|
||||
*/
|
||||
export class AlveoFactoryStatic {
|
||||
/**
|
||||
* Creates an instance of an Alveo application.
|
||||
*
|
||||
* @param rootModule The root module of the application.
|
||||
* @returns A promise that resolves to an AlveoApplication instance.
|
||||
*/
|
||||
public async create(rootModule: Constructor): Promise<AlveoApplication> {
|
||||
const container = new Container();
|
||||
|
||||
// 1. Register the module tree starting from the root module
|
||||
container.registerRootModule(rootModule);
|
||||
|
||||
// 2. Resolve all providers (this builds the graph and instantiates singletons)
|
||||
await container.resolveAll();
|
||||
|
||||
// 3. Wrap in the application instance
|
||||
const app = new AlveoApplication(container);
|
||||
|
||||
// 4. Initialize lifecycle hooks
|
||||
await app.init();
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static instance of AlveoFactory to be used by consumers.
|
||||
*/
|
||||
export const AlveoFactory = new AlveoFactoryStatic();
|
||||
19
src/index.ts
Normal file
19
src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export { Container } from "./container/Container";
|
||||
export type {
|
||||
ClassProvider,
|
||||
ExistingProvider,
|
||||
FactoryProvider,
|
||||
InjectableOptions,
|
||||
ModuleOptions,
|
||||
Provider,
|
||||
ProviderScope as Scope,
|
||||
ProviderToken,
|
||||
ValueProvider,
|
||||
} from "./container/types";
|
||||
export * from "./decorators";
|
||||
export * from "./errors";
|
||||
export { AlveoApplication } from "./factory/AlveoApplication";
|
||||
export { AlveoFactory } from "./factory/AlveoFactory";
|
||||
export * from "./lifecycle";
|
||||
export { ModuleRef } from "./module/ModuleRef";
|
||||
export type { Abstract, Constructor, Type } from "./types";
|
||||
15
src/lifecycle/index.ts
Normal file
15
src/lifecycle/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Interface defining a lifecycle hook that is called once the module has been initialized.
|
||||
* All dependencies have been resolved and instantiated at this point.
|
||||
*/
|
||||
export interface OnModuleInit {
|
||||
onModuleInit(): void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface defining a lifecycle hook that is called when the application is shutting down.
|
||||
* Useful for cleanup logic like closing database connections or stopping intervals.
|
||||
*/
|
||||
export interface OnModuleDestroy {
|
||||
onModuleDestroy(): void | Promise<void>;
|
||||
}
|
||||
37
src/module/ModuleRef.ts
Normal file
37
src/module/ModuleRef.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Container } from "../container/Container";
|
||||
import type { ProviderToken } from "../container/types";
|
||||
|
||||
/**
|
||||
* ModuleRef is an abstraction that allows for dynamic resolution of providers.
|
||||
* It is typically injected into services or controllers when dynamic lookup is needed.
|
||||
*/
|
||||
export abstract class ModuleRef {
|
||||
constructor(protected readonly container: Container) {}
|
||||
|
||||
/**
|
||||
* Retrieves an instance of a provider based on its token.
|
||||
*
|
||||
* @param token The token of the provider to retrieve.
|
||||
* @returns A promise that resolves to the provider instance.
|
||||
*/
|
||||
public abstract get<T>(token: ProviderToken<T>): Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A contextual implementation of ModuleRef that resolves providers within a specific module's scope.
|
||||
*/
|
||||
export class ContextualModuleRef extends ModuleRef {
|
||||
constructor(
|
||||
container: Container,
|
||||
private readonly moduleName: string,
|
||||
) {
|
||||
super(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the token within the context of the module this Ref belongs to.
|
||||
*/
|
||||
public override async get<T>(token: ProviderToken<T>): Promise<T> {
|
||||
return this.container.get(token, this.moduleName);
|
||||
}
|
||||
}
|
||||
10
src/types.ts
Normal file
10
src/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Required for constructor variance compatibility
|
||||
export type ConstructorArgs = any[];
|
||||
|
||||
export type Constructor<T = unknown> = new (...args: ConstructorArgs) => T;
|
||||
|
||||
export type Abstract<T = unknown> = abstract new (
|
||||
...args: ConstructorArgs
|
||||
) => T;
|
||||
|
||||
export type Type<T = unknown> = Constructor<T> | Abstract<T>;
|
||||
69
test.ts
Normal file
69
test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import "reflect-metadata";
|
||||
import {
|
||||
AlveoFactory,
|
||||
Inject,
|
||||
Injectable,
|
||||
Module,
|
||||
ModuleRef,
|
||||
type OnModuleInit,
|
||||
} from "./index";
|
||||
|
||||
const LOGGER_TOKEN = Symbol("LOGGER");
|
||||
|
||||
@Injectable()
|
||||
class LoggerService {
|
||||
log(message: string) {
|
||||
console.log(`[Logger]: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class DatabaseService implements OnModuleInit {
|
||||
constructor(
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: LoggerService,
|
||||
@Inject(ModuleRef) private readonly moduleRef: ModuleRef,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log("DatabaseService initialized.");
|
||||
const self = await this.moduleRef.get(DatabaseService);
|
||||
this.logger.log(`ModuleRef self-check: ${self === this}`);
|
||||
}
|
||||
|
||||
query() {
|
||||
return "data";
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TestService {
|
||||
constructor(private readonly logger: LoggerService) {
|
||||
this.logger.log("TestService initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
DatabaseService,
|
||||
LoggerService,
|
||||
{ provide: LOGGER_TOKEN, useExisting: LoggerService },
|
||||
],
|
||||
exports: [DatabaseService, LoggerService],
|
||||
})
|
||||
class DatabaseModule {}
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
providers: [TestService],
|
||||
})
|
||||
class AppModule {}
|
||||
|
||||
async function bootstrap() {
|
||||
console.log("Starting AlveoJS test...");
|
||||
const app = await AlveoFactory.create(AppModule);
|
||||
const db = await app.get(DatabaseService);
|
||||
console.log(`Query result: ${db.query()}`);
|
||||
await app.close();
|
||||
}
|
||||
|
||||
bootstrap().catch(console.error);
|
||||
31
tsconfig.json
Normal file
31
tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user