diff --git a/.gitignore b/.gitignore index 1261f4a..bcf561b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules lib coverage +.idea +.DS_Store diff --git a/packages/brandi/spec/binding-error.spec.ts b/packages/brandi/spec/binding-error.spec.ts new file mode 100644 index 0000000..53109e5 --- /dev/null +++ b/packages/brandi/spec/binding-error.spec.ts @@ -0,0 +1,88 @@ +import { Container, injected, token } from '../src'; + +describe('common container error', () => { + const INITIAL_ERR_MSG = 'error to stop logger module from being created' + + it('provides clear error when modules are functions', () => { + const createLoggerModule = () => { + throw new Error(INITIAL_ERR_MSG) + return { + log: (message: string) => { + // eslint-disable-next-line no-console + console.log(message) + }, + } + } + type Logger = ReturnType + + const createAdderModule = (logger: Logger) => { + return { + add: (a: number, b: number) => { + logger.log('adding') + return a + b + }, + } + } + type Adder = ReturnType + + const TOKENS = { + logger: token('logger'), + adder: token('adder'), + } + + const container = new Container() + injected(createAdderModule, TOKENS.logger) + container.bind(TOKENS.adder).toInstance(createAdderModule).inSingletonScope() + container.bind(TOKENS.logger).toInstance(createLoggerModule).inSingletonScope() + + expect(() => { + container.get(TOKENS.adder) + }).toThrow(new Error(`Failed to get token 'adder': +0. Failed to resolve the binding for 'adder' token. +1. Failed to resolve the binding for 'logger' token. +2. Failed to instantiate createLoggerModule: ${INITIAL_ERR_MSG}`)) + }) + + it('provides clear error when modules are classes', () => { + class LoggerModule { + constructor() { + throw new Error(INITIAL_ERR_MSG) + } + // eslint-disable-next-line + public log(message: string) { + // eslint-disable-next-line no-console + console.log(message); + } + } + + class AdderModule { + constructor( + public loggerModule: LoggerModule, + ) { + throw new Error('error to stop logger module from being created') + } + // eslint-disable-next-line + public add(a: number, b: number) { + this.loggerModule.log('adding') + return a + b + } + } + + const TOKENS = { + logger: token('logger'), + adder: token('adder'), + } + + const container = new Container() + injected(AdderModule, TOKENS.logger) + container.bind(TOKENS.adder).toInstance(AdderModule).inSingletonScope() + container.bind(TOKENS.logger).toInstance(LoggerModule).inSingletonScope() + + expect(() => { + container.get(TOKENS.adder) + }).toThrow(new Error(`Failed to get token 'adder': +0. Failed to resolve the binding for 'adder' token. +1. Failed to resolve the binding for 'logger' token. +2. Failed to instantiate LoggerModule: ${INITIAL_ERR_MSG}`)) + }) +}) diff --git a/packages/brandi/src/container/Container.ts b/packages/brandi/src/container/Container.ts index 14ce8c4..35995c8 100644 --- a/packages/brandi/src/container/Container.ts +++ b/packages/brandi/src/container/Container.ts @@ -12,6 +12,7 @@ import { isInstanceResolutionScopedBinding, isInstanceSingletonScopedBinding, } from './bindings'; +import { failedToGetTokenErrorMessage, isClass } from "../lib"; import { BindingsVault } from './BindingsVault'; import { DependencyModule } from './DependencyModule'; import { ResolutionCache } from './ResolutionCache'; @@ -109,7 +110,11 @@ export class Container extends DependencyModule { token: T, conditions?: ResolutionCondition[], ): TokenType { - return this.resolveToken(token, conditions) as TokenType; + try { + return this.resolveToken(token, conditions) as TokenType; + } catch(e) { + throw new Error(`Failed to get token '${token.__d}':\n${failedToGetTokenErrorMessage(e as Error)}`) + } } private resolveTokens( @@ -131,7 +136,13 @@ export class Container extends DependencyModule { ): unknown { const binding = this.vault.get(token, cache, conditions, target); - if (binding) return this.resolveBinding(binding, cache); + if (binding) { + try { + return this.resolveBinding(binding, cache); + } catch (e) { + throw new Error(`Failed to resolve the binding for '${token.__d}' token.`, { cause: e }) + } + } if (token.__o) return undefined; throw new Error(`No matching bindings found for '${token.__d}' token.`); @@ -231,16 +242,19 @@ export class Container extends DependencyModule { } try { - // @ts-expect-error: This expression is not callable. - const instance = creator(...parameters); - callableRegistry.set(creator, true); - return instance; - } catch { - // @ts-expect-error: This expression is not constructable. - // eslint-disable-next-line new-cap - const instance = new creator(...parameters); - callableRegistry.set(creator, false); + const creatorIsClass = isClass(creator) + const instance = creatorIsClass + ? // @ts-expect-error: This expression is not constructable. + // eslint-disable-next-line new-cap + new creator(...parameters) + // @ts-expect-error: This expression is not callable. + : creator(...parameters); + callableRegistry.set(creator, !creatorIsClass); return instance; + } catch (e) { + const error = e as Error + error.message = `Failed to instantiate ${creator.name}: ${error.message}` + throw error } } diff --git a/packages/brandi/src/lib.ts b/packages/brandi/src/lib.ts new file mode 100644 index 0000000..b63e77b --- /dev/null +++ b/packages/brandi/src/lib.ts @@ -0,0 +1,13 @@ +export function isClass(v: any) { + return typeof v === 'function' && /^\s*class\s+/.test(v.toString()); +} + +export function failedToGetTokenErrorMessage(e: Error) { + let error = e + const causes: Error[] = [] + while (error) { + causes.push(error) + error = error.cause as Error + } + return causes.map((c, idx) => `${idx}. ${c.message}`).join('\n') +}