feat: init project

This commit is contained in:
M1000fr
2026-01-10 17:27:30 +01:00
parent d02e52aed5
commit 00fb21c558
22 changed files with 957 additions and 5 deletions

339
src/container/Container.ts Normal file
View 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);
}
}
}