UNPKG

angular-style-injector

Version:

Dependency injection container inspired by Angular's Injector.

186 lines (185 loc) 8.76 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Injector = void 0; const injector_constant_1 = require("./injector.constant"); class Injector { constructor() { this.providers = new Map(); this.resolvers = new Map(); } /** * @description Creates a new instance of the `Injector` class. * * @param config Configuration object used to initialize the injector. * - `providers`: An array of provider definitions used to resolve dependencies. * - `parent` (optional): An optional parent injector to fall back to when a provider is not found locally. * - `name` (optional): A developer-defined name used for debugging and error reporting. * @returns A new `Injector` instance configured with the given providers and options. * * @remarks If no providers are passed, a warning will be logged to the console. **/ static create(config) { const injector = new Injector(); injector.parent = config.parent; injector.name = config.name; if (config.providers.length) { for (const provider of config.providers) { injector.provide(provider); } } else { console.warn(injector_constant_1.INJECTOR_ERRORS.EMPTY_PROVIDERS_WARN(config.name)); } return injector; } /** * @description Retrieves an instance from the injector based on the provided token. * * @param token The provider token used to retrieve the instance. * @param notFoundValue A fallback value to return if the token is not provided by this injector or its parents. * @param options Configuration object that influences how a dependency is resolved by the injector. * - `optional` (optional): If true, the injector returns `null` instead of throwing an error when the token was not found. * - `skipSelf` (optional): If true, the injector skips checking itself and instead delegates resolution to its parent (if presents). * - `self` (optional): If true, the injector only looks for the dependency in itself and does not check the parent injector (if presents). * @returns The resolved instance associated with the token. * @throws Error If the provider cannot be found in the current or parent injectors, and neither a `notFoundValue` * nor the `optional` flag was provided in the options. * * @remarks If the token is not found in the current injector, * the method delegates resolution to the parent injector (if present). **/ get(token, notFoundValue, options) { return this.internalGet(token, this.name, notFoundValue, options); } /** * @description This method was extracted as a separate method to preserve * the original injector's name `originName` and `notFoundValue` during * recursive resolution through the parent injector chain. **/ internalGet(token, originName, notFoundValue, options) { const { optional, self, skipSelf } = options ?? {}; const shouldCheckSelf = !skipSelf || self; const shouldCheckParent = !self; if (shouldCheckSelf) { const resolver = this.resolvers.get(token); if (resolver) { return resolver; } const provider = this.providers.get(token); if (provider) { this.resolve(token, provider, originName); return this.resolvers.get(token); } } if (shouldCheckParent && this.parent) { return this.parent.internalGet(token, originName, notFoundValue, { optional }); } if (notFoundValue !== undefined) { return notFoundValue; } if (optional) { return null; } throw new Error(injector_constant_1.INJECTOR_ERRORS.THROW_PROVIDER_NOT_FOUND(token, originName)); } /** * @description Registers a provider in the injector. * Handles both single and multi-provider configurations. * If a multi provider is added for an existing token, it merges the configurations into an array. * Clears any previously resolved instance for the given token to allow proper re resolution. * * @param providerConfig The provider configuration to register. **/ provide(providerConfig) { const token = typeof providerConfig === 'function' ? providerConfig : providerConfig.provide; if ((0, injector_constant_1.isSingleProvider)(providerConfig)) { // If config is: // 1. A class (constructor); // 2. Config does not have a "multi" field; // 3. "Multi" field is false. this.providers.set(token, providerConfig); } else { // Multi-provider: // 1. Config has "multi: true"; // 2. Need to be combined with other multi-providers by the same token. const existingProviderConfig = this.providers.get(token); if (Array.isArray(existingProviderConfig)) { // If there is already an array of providers, add a new one. existingProviderConfig.push(providerConfig); } else if (existingProviderConfig) { // If there is already one regular provider (not an array), turn it into an array + add a new one. this.providers.set(token, [existingProviderConfig, providerConfig]); } else { // There is no provider yet - create an array of one element. this.providers.set(token, [providerConfig]); } } // Clear the resolver cache for this token so that the next get() dependency is recreated with the new data. if (this.resolvers.has(token)) { this.resolvers.delete(token); } } /** * @description Resolves a provider by its token and stores the result in the internal cache (`resolvers`). * * @param token The token used to look up the provider. * @param provider The actual provider configuration(s) associated with the token. * @param originName The name of the injector that initiated the resolution. **/ resolve(token, provider, originName) { if (Array.isArray(provider)) { const resolvers = provider.map(config => this.getResolvedSingleProvider(config, originName)); this.resolvers.set(token, resolvers); } else { this.resolvers.set(token, this.getResolvedSingleProvider(provider, originName)); } } /** * @description Resolves a single provider configuration into its actual value or instance. * * @param providerConfig The configuration object or class constructor to resolve. * @param originName The name of the injector that initiated the resolution. * @returns The resolved instance or value for the provider. **/ getResolvedSingleProvider(providerConfig, originName) { if (typeof providerConfig === 'function') { return this.createClassInstance(providerConfig, originName); } else if ('useClass' in providerConfig) { return this.createClassInstance(providerConfig.useClass, originName); } else if ('useValue' in providerConfig) { return providerConfig.useValue; } else if ('useFactory' in providerConfig) { const depsList = providerConfig.deps ?? []; const resolvedDeps = depsList.map(token => this.internalGet(token, originName)); return providerConfig.useFactory(...resolvedDeps); } else { return this.internalGet(providerConfig.useExisting, originName); } } /** * @description Creates an instance of a dependency by resolving its constructor dependencies. * Uses `Reflect.getMetadata` to retrieve the list of dependencies defined in the constructor * and recursively resolves each dependency. * * @param constructor A class constructor * @param originName The name of the injector that initiated the resolution. * @return The class instance with resolved dependencies. **/ createClassInstance(constructor, originName) { if (!constructor.__injectable__) { throw new Error(injector_constant_1.INJECTOR_ERRORS.THROW_DECORATOR_MISSING(constructor.name)); } const depsList = Reflect.getMetadata('design:paramtypes', constructor) ?? []; const resolvedDeps = depsList.map(dependency => this.internalGet(dependency, originName)); return new constructor(...resolvedDeps); } } exports.Injector = Injector;