UNPKG

@wessberg/di

Version:

A compile-time powered Dependency-Injection container for Typescript that holds services and can produce instances of them as required.

209 lines (206 loc) 9.02 kB
// src/constant.ts var CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER = `___CTOR_ARGS___`; var CONSTRUCTOR_ARGUMENTS_SYMBOL = Symbol.for(CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER); var DI_COMPILER_ERROR_HINT = `Note: You must use DI-Compiler (https://github.com/wessberg/di-compiler) for this library to work correctly. Please consult the readme for instructions on how to install and configure it for your project.`; // src/error.ts var InstantiationError = class _InstantiationError extends TypeError { #name = "InstantiationError"; #originalError; #identifier; #parentChain; get [Symbol.toStringTag]() { return this.#name; } constructor(error, { identifier, parentChain, ...options }) { const message = typeof error === "string" ? error : error instanceof Error ? error.message : typeof error === "object" && error != null && "message" in error && typeof error.message === "string" ? error.message : void 0; const stack = typeof error === "string" ? void 0 : error instanceof Error ? error.stack : typeof error === "object" && error != null && "stack" in error && typeof error.stack === "string" ? error.stack : void 0; const originalError = error instanceof Error ? error : { message, stack }; super(message, options); this.#identifier = identifier; this.#originalError = originalError; this.#parentChain = parentChain != null && parentChain.length > 0 ? parentChain.map((item) => typeof item === "string" ? item : item.identifier) : [this.#identifier]; this.name = this[Symbol.toStringTag]; let currentOriginalError = this.#originalError; while (currentOriginalError instanceof _InstantiationError) { currentOriginalError = currentOriginalError.#originalError; } const lastService = this.#parentChain[this.#parentChain.length - 1]; const head = `Could not instantiate service: '${lastService}'`; const body = currentOriginalError.message == null || currentOriginalError.message.length < 1 ? "" : `: ${currentOriginalError.message}`; const tail = this.#parentChain.length > 1 ? ` Dependency chain: ${this.#parentChain.join(" -> ")}` : ""; this.message = `${head}${body}${tail}`; if (currentOriginalError.stack != null) { this.stack = currentOriginalError.stack; } } }; // src/util.ts function isClass(item) { if (typeof item !== "function") { return false; } const descriptor = Object.getOwnPropertyDescriptor(item, "prototype"); return !!descriptor && !descriptor.writable; } function isCustomConstructableService(item) { return !isClass(item) && typeof item === "function"; } // src/di-container.ts var DIContainer = class { get [Symbol.toStringTag]() { return "DIContainer"; } /** * A map between interface names and the services that should be dependency injected */ #constructorArguments = /* @__PURE__ */ new Map(); /** * A Map between identifying names for services and their IRegistrationRecords. */ #serviceRegistry = /* @__PURE__ */ new Map(); /** * A map between identifying names for services and concrete instances of their implementation. */ #instances = /* @__PURE__ */ new Map(); registerSingleton(newExpression, options) { if (options == null) { throw new ReferenceError(`2 arguments required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`); } if (newExpression == null) { return this.#register("SINGLETON", newExpression, options); } else { return this.#register("SINGLETON", newExpression, options); } } registerTransient(newExpression, options) { if (options == null) { throw new ReferenceError(`2 arguments required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`); } if (newExpression == null) { return this.#register("TRANSIENT", newExpression, options); } else { return this.#register("TRANSIENT", newExpression, options); } } /** * Gets an instance of the service matching the interface given as a generic type parameter. * For example, 'container.get<IFoo>()' returns a concrete instance of the implementation associated with the * generic interface name. * * You should not pass any options to the method if using the compiler. It will do that automatically. */ get(options) { if (options == null) { throw new ReferenceError(`1 argument required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`); } if (!this.has(options)) { throw new InstantiationError(`The service wasn't found in the registry.`, { identifier: options.identifier }); } return this.#constructInstance(options); } /** * Returns true if a service has been registered matching the interface given as a generic type parameter. * For example, 'container.get<IFoo>()' returns a concrete instance of the implementation associated with the * generic interface name. * * You should not pass any options to the method if using the compiler. It will do that automatically. */ // @ts-expect-error The 'T' type parameter is required for compile-time reflection, even though it is not part of the signature. has(options) { if (options == null) { throw new ReferenceError(`1 argument required, but only 0 present. ${DI_COMPILER_ERROR_HINT}`); } return this.#serviceRegistry.has(options.identifier); } #register(kind, newExpression, options) { const implementationArguments = "implementation" in options && options.implementation?.[CONSTRUCTOR_ARGUMENTS_SYMBOL] != null ? options.implementation[CONSTRUCTOR_ARGUMENTS_SYMBOL] : []; this.#constructorArguments.set(options.identifier, implementationArguments); this.#serviceRegistry.set( options.identifier, "implementation" in options && options.implementation != null ? { ...options, kind } : { ...options, kind, newExpression } ); } /** * Returns true if an instance exists that matches the given identifier. */ #hasInstance(identifier) { return this.#getInstance(identifier) != null; } /** * Gets the cached instance, if any, associated with the given identifier. */ #getInstance(identifier) { const instance = this.#instances.get(identifier); return instance == null ? null : instance; } /** * Gets an IRegistrationRecord associated with the given identifier. */ #getRegistrationRecord(identifier) { return this.#serviceRegistry.get(identifier); } /** * Caches the given instance so that it can be retrieved in the future. */ #setInstance(identifier, instance) { this.#instances.set(identifier, instance); return instance; } /** * Gets a lazy reference to another service */ #getLazyIdentifier(lazyPointer) { return new Proxy({}, { get: (_, key) => lazyPointer()[key] }); } /** * Constructs a new instance of the given identifier and returns it. * It checks the constructor arguments and injects any services it might depend on recursively. */ #constructInstance({ identifier, parentChain = [] }) { const registrationRecord = this.#getRegistrationRecord(identifier); if (registrationRecord == null) { return void 0; } if (this.#hasInstance(identifier) && registrationRecord.kind === "SINGLETON") { return this.#getInstance(identifier); } let instance; const me = { identifier, ref: this.#getLazyIdentifier(() => instance) }; const implementation = "newExpression" in registrationRecord ? registrationRecord.newExpression : registrationRecord.implementation; if (isClass(implementation)) { const mappedArgs = this.#constructorArguments.get(identifier); if (mappedArgs == null) { throw new InstantiationError(`Could not find constructor arguments. Have you registered it as a service?`, { identifier, parentChain }); } const instanceArgs = mappedArgs.map((dep) => { if (dep === void 0) return void 0; const matchedParent = parentChain.find((parent) => parent.identifier === dep); if (matchedParent != null) return matchedParent.ref; const nextParentChain = [...parentChain, me]; const constructedInstance = this.#constructInstance({ identifier: dep, parentChain: nextParentChain }); if (constructedInstance == null) { throw new InstantiationError(`Dependency '${dep}' was not found in the service registry.`, { identifier: me.identifier, parentChain: nextParentChain }); } return constructedInstance; }); instance = new implementation(...instanceArgs); } else if (isCustomConstructableService(implementation)) { instance = implementation(); } else { throw new InstantiationError(`No implementation was given!`, { identifier, parentChain }); } return registrationRecord.kind === "SINGLETON" ? this.#setInstance(identifier, instance) : instance; } }; export { CONSTRUCTOR_ARGUMENTS_SYMBOL, CONSTRUCTOR_ARGUMENTS_SYMBOL_IDENTIFIER, DIContainer }; //# sourceMappingURL=index.js.map