feat: implement Dynamic Modules support (sync and async)

Allows modules to be configured at runtime via static methods returning DynamicModule objects, similar to NestJS forRoot/register patterns.
This commit is contained in:
M1000fr
2026-01-11 17:18:15 +01:00
parent 7452f21450
commit deb9e704fb
4 changed files with 167 additions and 26 deletions

View File

@@ -2,8 +2,10 @@ import "reflect-metadata";
import { CircularDependencyError, ProviderNotFoundError } from "../errors";
import { ContextualModuleRef, ModuleRef } from "../module/ModuleRef";
import type { Constructor } from "../types";
import type { ForwardRefFn } from "./forwardRef";
import {
type BaseProvider,
type DynamicModule,
INJECT_METADATA_KEY,
INJECTABLE_METADATA_KEY,
MODULE_METADATA_KEY,
@@ -40,34 +42,95 @@ export class Container {
this.ensureScope(this.globalContext);
}
public registerRootModule(moduleClass: Constructor): void {
this.rootModuleName = moduleClass.name;
this.registerModule(moduleClass);
public async registerRootModule(
module: Constructor | DynamicModule,
): Promise<void> {
const moduleClass = (module as DynamicModule).module || module;
this.rootModuleName = (moduleClass as Constructor).name;
await this.registerModule(module);
}
public getRootModuleName(): string | undefined {
return this.rootModuleName;
}
public registerModule(moduleClass: Constructor): void {
const options: ModuleOptions | undefined = Reflect.getMetadata(
public async registerModule(
module:
| Constructor
| DynamicModule
| Promise<DynamicModule>
| ForwardRefFn<Constructor>,
): Promise<void> {
const unwrappedModule = (await (typeof module === "object" &&
module !== null &&
"forwardRef" in (module as object)
? (
module as { forwardRef: () => Constructor | DynamicModule }
).forwardRef()
: module)) as Constructor | DynamicModule;
const isDynamic =
typeof unwrappedModule === "object" && "module" in unwrappedModule;
const moduleClass = isDynamic
? (unwrappedModule as DynamicModule).module
: (unwrappedModule as Constructor);
const dynamicOptions: ModuleOptions = isDynamic
? (unwrappedModule as DynamicModule)
: {};
const metadataOptions: ModuleOptions | undefined = Reflect.getMetadata(
MODULE_METADATA_KEY,
moduleClass,
);
if (!options) return;
if (!metadataOptions && !isDynamic) return;
const moduleName = moduleClass.name;
if (this.moduleOptions.has(moduleName)) return;
const rawImports = [
...(metadataOptions?.imports || []),
...(dynamicOptions?.imports || []),
];
const resolvedImports = await Promise.all(
rawImports.map(async (imp) => {
return await (typeof imp === "object" &&
imp !== null &&
"forwardRef" in (imp as object)
? (
imp as {
forwardRef: () => Constructor | DynamicModule;
}
).forwardRef()
: imp);
}),
);
const options: ModuleOptions = {
...metadataOptions,
...dynamicOptions,
imports: resolvedImports,
providers: [
...(metadataOptions?.providers || []),
...(dynamicOptions?.providers || []),
],
exports: [
...(metadataOptions?.exports || []),
...(dynamicOptions?.exports || []),
],
};
this.moduleOptions.set(moduleName, options);
this.ensureScope(moduleName);
if (options.imports) {
for (const imported of options.imports) {
this.registerModule(
this.unwrapToken(imported as ProviderToken<Constructor>),
);
}
for (const imported of resolvedImports) {
await this.registerModule(
imported as
| Constructor
| DynamicModule
| Promise<DynamicModule>,
);
}
if (options.providers) {
@@ -292,15 +355,16 @@ export class Container {
const options = this.moduleOptions.get(context);
if (options?.imports) {
for (const imported of options.imports) {
const unwrappedImport = this.unwrapToken(
imported as ProviderToken<Constructor>,
);
const importedProviders = this.providers.get(
unwrappedImport.name,
);
const importedOptions = this.moduleOptions.get(
unwrappedImport.name,
);
const importedModule =
(imported as DynamicModule).module ||
(imported as Constructor);
if (typeof importedModule !== "function") continue;
const moduleName = importedModule.name;
const importedProviders = this.providers.get(moduleName);
const importedOptions = this.moduleOptions.get(moduleName);
if (
importedProviders?.has(unwrappedToken) &&
importedOptions?.exports?.some(
@@ -310,7 +374,7 @@ export class Container {
) {
return importedProviders.get(unwrappedToken);
}
if (unwrappedToken === unwrappedImport) {
if (unwrappedToken === importedModule) {
return importedProviders?.get(unwrappedToken);
}
}