@elsikora/cladi
Version:
Zero-dependency TypeScript DI toolkit with typed tokens and scoped lifecycles.
189 lines (185 loc) • 8.22 kB
JavaScript
'use strict';
var error_class = require('../../../infrastructure/class/base/error.class.js');
const IMPORT_REGISTRATION_MODE_RANK = 1;
const ROOT_REGISTRATION_MODE_RANK = 2;
const MODULE_REGISTRATION_MODE_RANK = {
import: IMPORT_REGISTRATION_MODE_RANK,
root: ROOT_REGISTRATION_MODE_RANK,
};
/**
* Registers module trees in deterministic order (imports first, then local providers).
* Imported modules expose providers through the `exports` contract.
* @param {IDIContainer} container Target container.
* @param {Array<IDIModule>} modules Module list to compose.
*/
function composeModules(container, modules) {
const processingModules = new Set();
const registeredModuleModes = new WeakMap();
const registeredModuleProvidersByToken = new WeakMap();
const registerModule = (moduleDefinition, mode, modulePath) => {
const previousMode = registeredModuleModes.get(moduleDefinition);
const hasSufficientRegistrationMode = previousMode !== undefined && MODULE_REGISTRATION_MODE_RANK[previousMode] >= MODULE_REGISTRATION_MODE_RANK[mode];
if (hasSufficientRegistrationMode) {
return;
}
if (processingModules.has(moduleDefinition)) {
throw new error_class.BaseError("Circular module imports detected", {
code: "MODULE_CIRCULAR_DEPENDENCY",
context: {
cyclePath: getModuleCyclePath(modulePath, moduleDefinition),
},
source: "composeModules",
});
}
processingModules.add(moduleDefinition);
try {
const localProvidersByToken = getLocalProvidersByToken(moduleDefinition);
validateModuleExports(moduleDefinition, localProvidersByToken);
const nextModulePath = [...modulePath, moduleDefinition];
for (const importedModule of moduleDefinition.imports) {
registerModule(importedModule, "import", nextModulePath);
}
const providersToRegister = mode === "root" ? moduleDefinition.providers : collectImportableProviders(moduleDefinition, localProvidersByToken);
const previouslyRegisteredProvidersByToken = registeredModuleProvidersByToken.get(moduleDefinition) ?? new Map();
const nextRegisteredProvidersByToken = new Map(previouslyRegisteredProvidersByToken);
for (const provider of providersToRegister) {
const previouslyRegisteredProviders = nextRegisteredProvidersByToken.get(provider.provide) ?? new Set();
if (previouslyRegisteredProviders.has(provider)) {
continue;
}
container.register(provider);
const nextProviders = new Set(previouslyRegisteredProviders);
nextProviders.add(provider);
nextRegisteredProvidersByToken.set(provider.provide, nextProviders);
}
registeredModuleModes.set(moduleDefinition, mode);
registeredModuleProvidersByToken.set(moduleDefinition, nextRegisteredProvidersByToken);
}
finally {
processingModules.delete(moduleDefinition);
}
};
for (const moduleDefinition of modules) {
registerModule(moduleDefinition, "root", []);
}
}
/**
* Selects provider subset visible to an importing module.
* @param {IDIModule} moduleDefinition Source module definition.
* @param {Map<symbol, Array<Provider>>} localProvidersByToken Local providers indexed by token.
* @returns {Array<Provider>} Importable providers for current module.
*/
function collectImportableProviders(moduleDefinition, localProvidersByToken) {
const importableTokens = new Set();
const pendingTokens = [...moduleDefinition.exports];
while (pendingTokens.length > 0) {
const currentToken = pendingTokens.pop();
if (!currentToken || importableTokens.has(currentToken)) {
continue;
}
importableTokens.add(currentToken);
const currentProviders = localProvidersByToken.get(currentToken);
if (!currentProviders) {
continue;
}
for (const currentProvider of currentProviders) {
const dependencies = getProviderDependencies(currentProvider);
for (const dependencyToken of dependencies) {
if (localProvidersByToken.has(dependencyToken)) {
pendingTokens.push(dependencyToken);
}
}
}
}
return moduleDefinition.providers.filter((provider) => importableTokens.has(provider.provide));
}
/**
* Converts token symbol to readable debug representation.
* @param {Token<unknown>} token Provider token.
* @returns {string} Human readable token string.
*/
function describeToken(token) {
return token.description ? `Symbol(${token.description})` : token.toString();
}
/**
* Builds map of local providers by token.
* @param {IDIModule} moduleDefinition Module definition.
* @returns {Map<symbol, Array<Provider>>} Providers indexed by token.
*/
function getLocalProvidersByToken(moduleDefinition) {
const providersByToken = new Map();
for (const provider of moduleDefinition.providers) {
const providersForToken = providersByToken.get(provider.provide);
if (providersForToken) {
providersForToken.push(provider);
continue;
}
providersByToken.set(provider.provide, [provider]);
}
return providersByToken;
}
/**
* Builds a module cycle path for structured error context.
* @param {Array<IDIModule>} modulePath Current traversal path.
* @param {IDIModule} repeatedModule Repeated module that closes the cycle.
* @returns {Array<string>} Human readable cycle chain.
*/
function getModuleCyclePath(modulePath, repeatedModule) {
const cycleStartIndex = modulePath.indexOf(repeatedModule);
const cycleModules = cycleStartIndex === -1 ? [...modulePath, repeatedModule] : [...modulePath.slice(cycleStartIndex), repeatedModule];
return cycleModules.map((cycleModule) => getModuleName(cycleModule));
}
/**
* Returns readable module identifier for diagnostics.
* @param {IDIModule} moduleDefinition Module definition.
* @returns {string} Module name for diagnostics.
*/
function getModuleName(moduleDefinition) {
return moduleDefinition.name ?? "anonymous-module";
}
/**
* Returns direct dependency tokens required by provider strategy.
* @param {Provider} provider Provider definition.
* @returns {Array<Token<unknown>>} Direct dependency token list.
*/
function getProviderDependencies(provider) {
if ("useExisting" in provider) {
return [provider.useExisting];
}
if ("useLazy" in provider) {
return [provider.useLazy];
}
if ("deps" in provider) {
return provider.deps ?? [];
}
return [];
}
/**
* Ensures each exported token is either local provider token or re-export from imports.
* @param {IDIModule} moduleDefinition Module definition.
* @param {Map<symbol, Array<Provider>>} localProvidersByToken Local providers indexed by token.
*/
function validateModuleExports(moduleDefinition, localProvidersByToken) {
const importedExportTokens = new Set();
for (const importedModule of moduleDefinition.imports) {
for (const exportedToken of importedModule.exports) {
importedExportTokens.add(exportedToken);
}
}
for (const exportedToken of moduleDefinition.exports) {
const hasLocalProvider = localProvidersByToken.has(exportedToken);
const hasImportedExport = importedExportTokens.has(exportedToken);
if (!hasLocalProvider && !hasImportedExport) {
throw new error_class.BaseError("Invalid module exports: token is not provided locally or by imported modules", {
code: "MODULE_EXPORT_INVALID",
context: {
moduleName: moduleDefinition.name ?? "anonymous-module",
token: describeToken(exportedToken),
},
source: "composeModules",
});
}
}
}
exports.composeModules = composeModules;
//# sourceMappingURL=compose-modules.utility.js.map