UNPKG

@elsikora/cladi

Version:

Zero-dependency TypeScript DI toolkit with typed tokens and scoped lifecycles.

189 lines (185 loc) 8.22 kB
'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