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

34
.gitignore vendored Normal file
View 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
View 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"]
}
}
}
}

View File

@@ -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
View 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
View 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=="],
}
}

1
index.ts Normal file
View File

@@ -0,0 +1 @@
export * from "./src";

21
package.json Normal file
View 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
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);
}
}
}

60
src/container/types.ts Normal file
View 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
View File

@@ -0,0 +1,3 @@
export * from "./inject.decorator";
export * from "./injectable.decorator";
export * from "./module.decorator";

View 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);
};
}

View 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);
};
}

View 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
View 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}`);
}
}

View 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");
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}