UNPKG

@shahen.poghosyan/awilix

Version:

Extremely powerful dependency injection container.

1,558 lines (1,548 loc) 50.6 kB
import { sync } from 'glob'; import { basename, resolve } from 'path'; import * as util from 'util'; import { inspect } from 'util'; /** * Newline. */ const EOL = '\n'; /** * An extendable error class. * @author https://github.com/bjyoungblood/es6-error/ */ class ExtendableError extends Error { /** * Constructor for the error. * * @param {String} message * The error message. */ constructor(message) { super(message); // extending Error is weird and does not propagate `message` Object.defineProperty(this, 'message', { enumerable: false, value: message }); Object.defineProperty(this, 'name', { enumerable: false, value: this.constructor.name }); Error.captureStackTrace(this, this.constructor); } } /** * Base error for all Awilix-specific errors. */ class AwilixError extends ExtendableError { } /** * Error thrown to indicate a type mismatch. * TODO(v3): remove `AwilixNotAFunctionError` and use this. */ class AwilixTypeError extends AwilixError { /** * Constructor, takes the function name, expected and given * type to produce an error. * * @param {string} funcDescription * Name of the function being guarded. * * @param {string} paramName * The parameter there was an issue with. * * @param {string} expectedType * Name of the expected type. * * @param {string} givenType * Name of the given type. */ constructor(funcDescription, paramName, expectedType, givenType) { super(`${funcDescription}: expected ${paramName} to be ${expectedType}, but got ${givenType}.`); } /** * Asserts the given condition, throws an error otherwise. * * @param {*} condition * The condition to check * * @param {string} funcDescription * Name of the function being guarded. * * @param {string} paramName * The parameter there was an issue with. * * @param {string} expectedType * Name of the expected type. * * @param {string} givenType * Name of the given type. */ static assert(condition, funcDescription, paramName, expectedType, givenType) { if (!condition) { throw new AwilixTypeError(funcDescription, paramName, expectedType, givenType); } return condition; } } /** * A nice error class so we can do an instanceOf check. */ class AwilixResolutionError extends AwilixError { /** * Constructor, takes the registered modules and unresolved tokens * to create a message. * * @param {string|symbol} name * The name of the module that could not be resolved. * * @param {string[]} resolutionStack * The current resolution stack */ constructor(name, resolutionStack, message) { if (typeof name === 'symbol') { name = name.toString(); } resolutionStack = resolutionStack.slice(); resolutionStack.push(name); const resolutionPathString = resolutionStack.join(' -> '); let msg = `Could not resolve '${name}'.`; if (message) { msg += ` ${message}`; } msg += EOL + EOL; msg += `Resolution path: ${resolutionPathString}`; super(msg); } } /** * Creates a tokenizer for the specified source. * * @param source */ function createTokenizer(source) { const end = source.length; let pos = 0; let type = 'EOF'; let value = ''; let flags = 0 /* None */; // These are used to greedily skip as much as possible. // Whenever we reach a paren, we increment these. let parenLeft = 0; let parenRight = 0; return { next, done }; /** * Advances the tokenizer and returns the next token. */ function next(nextFlags = 0 /* None */) { flags = nextFlags; advance(); return createToken(); } /** * Advances the tokenizer state. */ function advance() { value = ''; type = 'EOF'; while (true) { if (pos >= end) { return (type = 'EOF'); } let ch = source.charAt(pos); // Whitespace is irrelevant if (isWhiteSpace(ch)) { pos++; continue; } switch (ch) { case '(': pos++; parenLeft++; return (type = ch); case ')': pos++; parenRight++; return (type = ch); case '*': pos++; return (type = ch); case ',': pos++; return (type = ch); case '=': pos++; if ((flags & 1 /* Dumb */) === 0) { // Not in dumb-mode, so attempt to skip. skipExpression(); } // We need to know that there's a default value so we can // skip it if it does not exist when resolving. return (type = ch); case '/': pos++; const nextCh = source.charAt(pos); if (nextCh === '/') { skipUntil(c => c === '\n'); pos++; } if (nextCh === '*') { skipUntil(c => { const closing = source.charAt(pos + 1); return c === '*' && closing === '/'; }); pos++; } continue; default: // Scans an identifier. if (isIdentifierStart(ch)) { scanIdentifier(); return type; } // Elegantly skip over tokens we don't care about. pos++; } } } /** * Scans an identifier, given it's already been proven * we are ready to do so. */ function scanIdentifier() { const identStart = source.charAt(pos); const start = ++pos; while (isIdentifierPart(source.charAt(pos))) { pos++; } value = '' + identStart + source.substring(start, pos); type = value === 'function' || value === 'class' ? value : 'ident'; if (type !== 'ident') { value = ''; } return value; } /** * Skips everything until the next comma or the end of the parameter list. * Checks the parenthesis balance so we correctly skip function calls. */ function skipExpression() { skipUntil(ch => { const isAtRoot = parenLeft === parenRight + 1; if (ch === ',' && isAtRoot) { return true; } if (ch === '(') { parenLeft++; return false; } if (ch === ')') { parenRight++; if (isAtRoot) { return true; } } return false; }); } /** * Skips strings and whilespace until the predicate is true. */ function skipUntil(callback) { while (pos < source.length) { let ch = source.charAt(pos); if (callback(ch)) { return; } if (isWhiteSpace(ch)) { pos++; continue; } if (isStringQuote(ch)) { skipString(); continue; } pos++; } } /** * Given the current position is at a string quote, skips the entire string. */ function skipString() { const quote = source.charAt(pos); pos++; while (pos < source.length) { const ch = source.charAt(pos); const prev = source.charAt(pos - 1); // Checks if the quote was escaped. if (ch === quote && prev !== '\\') { pos++; return; } // Template strings are a bit tougher, we want to skip the interpolated values. if (quote === '`') { const next = source.charAt(pos + 1); if (next === '$') { const afterDollar = source.charAt(pos + 2); if (afterDollar === '{') { // This is the start of an interpolation; skip the ${ pos = pos + 2; // Skip strings and whitespace until we reach the ending }. // This includes skipping nested interpolated strings. :D skipUntil(ch => ch === '}'); } } } pos++; } } /** * Creates a token from the current state. */ function createToken() { if (value) { return { value, type }; } return { type }; } /** * Determines if we are done parsing. */ function done() { return type === 'EOF'; } } /** * Determines if the given character is a whitespace character. * * @param {string} ch * @return {Boolean} */ function isWhiteSpace(ch) { switch (ch) { case '\r': case '\n': case ' ': return true; } return false; } /** * Determines if the specified character is a string quote. * @param {string} ch * @return {Boolean} */ function isStringQuote(ch) { switch (ch) { case "'": case '"': case '`': return true; } return false; } // NOTE: I've added the `.` character so that member expression paths // are seen as identifiers. This is so we don't get a constructor token for // stuff like `MyClass.prototype.constructor()` const IDENT_START_EXPR = /^[_$a-zA-Z\xA0-\uFFFF]$/; const IDENT_PART_EXPR = /^[._$a-zA-Z0-9\xA0-\uFFFF]$/; /** * Determines if the character is a valid JS identifier start character. */ function isIdentifierStart(ch) { return IDENT_START_EXPR.test(ch); } /** * Determines if the character is a valid JS identifier start character. */ function isIdentifierPart(ch) { return IDENT_PART_EXPR.test(ch); } /** * Quick flatten utility to flatten a 2-dimensional array. * * @param {Array<Array<Item>>} array * The array to flatten. * * @return {Array<Item>} * The flattened array. */ function flatten(array) { const result = []; array.forEach(arr => { arr.forEach(item => { result.push(item); }); }); return result; } /** * Creates a { name: value } object if the input isn't already in that format. * * @param {string|object} name * Either a string or an object. * * @param {*} value * The value, only used if name is not an object. * * @return {object} */ function nameValueToObject(name, value) { let obj = name; if (typeof obj === 'string' || typeof obj === 'symbol') { return { [name]: value }; } return obj; } /** * Returns the last item in the array. * * @param {*[]} arr * The array. * * @return {*} * The last element. */ function last(arr) { return arr[arr.length - 1]; } /** * Determines if the given function is a class. * * @param {Function} fn * @return {Boolean} */ function isClass(fn) { /*tslint:disable-next-line*/ if (typeof fn !== 'function') { return false; } // Should only need 2 tokens. const tokenizer = createTokenizer(fn.toString()); const first = tokenizer.next(); if (first.type === 'class') { return true; } const second = tokenizer.next(); if (first.type === 'function' && second.value) { if (second.value[0] === second.value[0].toUpperCase()) { return true; } } return false; } /** * Determines if the given value is a function. * * @param {Any} val * Any value to check if it's a function. * * @return {Boolean} * true if the value is a function, false otherwise. */ function isFunction(val) { return typeof val === 'function'; } /** * Returns the unique items in the array. * * @param {Array<T>} * The array to remove dupes from. * * @return {Array<T>} * The deduped array. */ function uniq(arr) { const result = []; for (const idx in arr) { const item = arr[idx]; if (result.indexOf(item) === -1) { result.push(item); } } return result; } // Regex to extract the module name. const nameExpr = /(.*)\..*/i; /** * Internal method for globbing a single pattern. * * @param {String} globPattern * The glob pattern. * * @param {String} opts.cwd * Current working directory, used for resolving filepaths. * Defaults to `process.cwd()`. * * @return {[{name, path, opts}]} * The module names and paths. * * @api private */ function _listModules(globPattern, opts) { opts = Object.assign({ cwd: process.cwd(), glob: sync }, opts); let patternOpts = null; if (globPattern instanceof Array) { patternOpts = globPattern[1]; globPattern = globPattern[0]; } const result = opts.glob(globPattern, { cwd: opts.cwd }); const mapped = result.map(p => ({ name: nameExpr.exec(basename(p))[1], path: resolve(opts.cwd, p), opts: patternOpts })); return mapped; } /** * Returns a list of {name, path} pairs, * where the name is the module name, and path is the actual * full path to the module. * * @param {String|Array<String>} globPatterns * The glob pattern as a string or an array of strings. * * @param {String} opts.cwd * Current working directory, used for resolving filepaths. * Defaults to `process.cwd()`. * * @return {[{name, path}]} * An array of objects with the module names and paths. */ function listModules(globPatterns, opts) { if (Array.isArray(globPatterns)) { return flatten(globPatterns.map(p => _listModules(p, opts))); } return _listModules(globPatterns, opts); } /** * Lifetime types. */ const Lifetime = { /** * The registration will be resolved once and only once. * @type {String} */ SINGLETON: 'SINGLETON', /** * The registration will be resolved every time (never cached). * @type {String} */ TRANSIENT: 'TRANSIENT', /** * The registration will be resolved once per scope. * @type {String} */ SCOPED: 'SCOPED' }; /** * Resolution modes. */ const InjectionMode = { /** * The dependencies will be resolved by injecting the cradle proxy. * * @type {String} */ PROXY: 'PROXY', /** * The dependencies will be resolved by inspecting parameter names of the function/constructor. * * @type {String} */ CLASSIC: 'CLASSIC' }; /* * Parses the parameter list of a function string, including ES6 class constructors. * * @param {string} source * The source of a function to extract the parameter list from * * @return {Array<Parameter>} * Returns an array of parameters. */ function parseParameterList(source) { const { next: _next, done } = createTokenizer(source); const params = []; let t = null; nextToken(); while (!done()) { switch (t.type) { case 'class': skipUntilConstructor(); // Next token is the constructor identifier. nextToken(); break; case 'function': const next = nextToken(); if (next.type === 'ident' || next.type === '*') { // This is the function name or a generator star. Skip it. nextToken(); } break; case '(': // Start parsing parameter names. parseParams(); break; case ')': // We're now out of the parameter list. return params; case 'ident': // Likely a paren-less arrow function // which can have no default args. const param = { name: t.value, optional: false }; if (t.value === 'async') { // Given it's the very first token, we can assume it's an async function, // so skip the async keyword if the next token is not an equals sign, in which // case it is a single-arg arrow func. const next = nextToken(); if (next && next.type !== '=') { break; } } params.push(param); return params; /* istanbul ignore next */ default: throw unexpected(); } } return params; /** * After having been placed within the parameter list of * a function, parses the parameters. */ function parseParams() { // Current token is a left-paren let param = { name: '', optional: false }; while (!done()) { nextToken(); switch (t.type) { case 'ident': param.name = t.value; break; case '=': param.optional = true; break; case ',': params.push(param); param = { name: '', optional: false }; break; case ')': if (param.name) { params.push(param); } return; /* istanbul ignore next */ default: throw unexpected(); } } } /** * Skips until we reach the constructor identifier. */ function skipUntilConstructor() { while (!isConstructorToken() && !done()) { nextToken(1 /* Dumb */); } } /** * Determines if the current token represents a constructor, and the next token after it is a paren */ function isConstructorToken() { return t.type === 'ident' && t.value === 'constructor'; } /** * Advances the tokenizer and stores the previous token in history */ function nextToken(flags = 0 /* None */) { t = _next(flags); return t; } /** * Returns an error describing an unexpected token. */ /* istanbul ignore next */ function unexpected() { return new SyntaxError(`Parsing parameter list, did not expect ${t.type} token${t.value ? ` (${t.value})` : ''}`); } } /** * RESOLVER symbol can be used by modules loaded by * `loadModules` to configure their lifetime, injection mode, etc. */ const RESOLVER = Symbol('Awilix Resolver Config'); /** * Creates a simple value resolver where the given value will always be resolved. * * @param {string} name * The name to register the value as. * * @param {*} value * The value to resolve. * * @return {object} * The resolver. */ function asValue(value) { return { resolve: () => value }; } /** * Creates a factory resolver, where the given factory function * will be invoked with `new` when requested. * * @param {string} name * The name to register the value as. * * @param {Function} fn * The function to register. * * @param {object} opts * Additional options for the resolver. * * @return {object} * The resolver. */ function asFunction(fn, opts) { if (!isFunction(fn)) { throw new AwilixTypeError('asFunction', 'fn', 'function', fn); } const defaults = { lifetime: Lifetime.TRANSIENT }; opts = makeOptions(defaults, opts, fn[RESOLVER]); const resolve = generateResolve(fn); let result = Object.assign({ resolve }, opts); return createDisposableResolver(createBuildResolver(result)); } /** * Like a factory resolver, but for classes that require `new`. * * @param {string} name * The name to register the value as. * * @param {Class} Type * The function to register. * * @param {object} opts * Additional options for the resolver. * * @return {object} * The resolver. */ function asClass(Type, opts) { if (!isFunction(Type)) { throw new AwilixTypeError('asClass', 'Type', 'class', Type); } const defaults = { lifetime: Lifetime.TRANSIENT }; opts = makeOptions(defaults, opts, Type[RESOLVER]); // A function to handle object construction for us, as to make the generateResolve more reusable const newClass = function newClass() { return Reflect.construct(Type, arguments); }; const resolve = makeResolveLazy(generateResolve(newClass, Type)); return createDisposableResolver(createBuildResolver(Object.assign({}, opts, { resolve }))); } /** * Resolves to the specified registration. */ function aliasTo(name) { return { resolve(container) { return container.resolve(name); } }; } /** * Given an options object, creates a fluid interface * to manage it. * * @param {*} obj * The object to return. * * @return {object} * The interface. */ function createBuildResolver(obj) { function setLifetime(value) { return createBuildResolver(Object.assign({}, this, { lifetime: value })); } function setInjectionMode(value) { return createBuildResolver(Object.assign({}, this, { injectionMode: value })); } function inject(injector) { return createBuildResolver(Object.assign({}, this, { injector })); } return updateResolver(obj, { setLifetime, inject, transient: partial(setLifetime, Lifetime.TRANSIENT), scoped: partial(setLifetime, Lifetime.SCOPED), singleton: partial(setLifetime, Lifetime.SINGLETON), setInjectionMode, proxy: partial(setInjectionMode, InjectionMode.PROXY), classic: partial(setInjectionMode, InjectionMode.CLASSIC) }); } /** * Given a resolver, returns an object with methods to manage the disposer * function. * @param obj */ function createDisposableResolver(obj) { function disposer(dispose) { return createDisposableResolver(Object.assign({}, this, { dispose })); } return updateResolver(obj, { disposer }); } /** * Partially apply arguments to the given function. */ function partial(fn, arg1) { return function partiallyApplied() { return fn.call(this, arg1); }; } /** * Makes an options object based on defaults. * * @param {object} defaults * Default options. * * @param {...} rest * The input to check and possibly assign to the resulting object * * @return {object} */ function makeOptions(defaults, ...rest) { return Object.assign({}, defaults, ...rest); } /** * Creates a new resolver with props merged from both. * * @param source * @param target */ function updateResolver(source, target) { const result = Object.assign({}, source, target); return result; } /** * Returns a wrapped `resolve` function that provides values * from the injector and defers to `container.resolve`. * * @param {AwilixContainer} container * @param {Object} locals * @return {Function} */ function wrapWithLocals(container, locals) { return function wrappedResolve(name, resolveOpts) { if (name in locals) { return locals[name]; } return container.resolve(name, resolveOpts); }; } /** * Returns a new Proxy that checks the result from `injector` * for values before delegating to the actual container. * * @param {Object} cradle * @param {Function} injector * @return {Proxy} */ function createInjectorProxy(container, injector) { const locals = injector(container); const allKeys = uniq([ ...Reflect.ownKeys(container.cradle), ...Reflect.ownKeys(locals) ]); // TODO: Lots of duplication here from the container proxy. // Need to refactor. const proxy = new Proxy({}, { /** * Resolves the value by first checking the locals, then the container. */ get(target, name) { if (name === Symbol.iterator) { return function* iterateRegistrationsAndLocals() { for (const prop in container.cradle) { yield prop; } for (const prop in locals) { yield prop; } }; } if (name in locals) { return locals[name]; } return container.resolve(name); }, /** * Used for `Object.keys`. */ ownKeys() { return allKeys; }, /** * Used for `Object.keys`. */ getOwnPropertyDescriptor(target, key) { if (allKeys.indexOf(key) > -1) { return { enumerable: true, configurable: true }; } return undefined; } }); return proxy; } /** * Makes dependency resolution lazy by wrapping the resolved value of original resolve function in a Proxy object * * @this {Registration} * The `this` context is a resolver. * * @param {Function} originalResolve * The original function for dependency resolution * * @return {Function} * The function used for lazy dependency resolution */ function makeResolveLazy(originalResolve) { let resolved = null; return function resolve(container) { let self = this; return new Proxy({}, { construct(_, argArray, newTarget) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.construct(resolved, argArray, newTarget); }, defineProperty(_, p, attributes) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.defineProperty(resolved, p, attributes); }, deleteProperty(_, propertyKey) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.deleteProperty(resolved, propertyKey); }, enumerate(_) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.enumerate(resolved); }, get(_, name) { if (!resolved) { resolved = originalResolve.call(self, container); } return resolved[name]; }, getOwnPropertyDescriptor(_, p) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.getOwnPropertyDescriptor(resolved, p); }, getPrototypeOf() { if (!resolved) { resolved = originalResolve.call(self, container); } return Object.getPrototypeOf(resolved); }, has(_, p) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.has(resolved, p); }, isExtensible() { if (!resolved) { resolved = originalResolve.call(self, container); } return resolved.isExtensible(); }, ownKeys(_) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.ownKeys(resolved); }, preventExtensions(_) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.preventExtensions(resolved); }, set(_, p, value) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.set(resolved, p, value); }, setPrototypeOf(_, proto) { if (!resolved) { resolved = originalResolve.call(self, container); } return Reflect.setPrototypeOf(resolved, proto); } }); }; } /** * Returns a resolve function used to construct the dependency graph * * @this {Registration} * The `this` context is a resolver. * * @param {Function} fn * The function to construct * * @param {Function} dependencyParseTarget * The function to parse for the dependencies of the construction target * * @param {boolean} isFunction * Is the resolution target an actual function or a mask for a constructor? * * @return {Function} * The function used for dependency resolution */ function generateResolve(fn, dependencyParseTarget) { // If the function used for dependency parsing is falsy, use the supplied function if (!dependencyParseTarget) { dependencyParseTarget = fn; } // Parse out the dependencies // NOTE: we do this regardless of whether PROXY is used or not, // because if this fails, we want it to fail early (at startup) rather // than at resolution time. const dependencies = parseDependencies(dependencyParseTarget); // Use a regular function instead of an arrow function to facilitate binding to the resolver. return function resolve(container) { // Because the container holds a global reolutionMode we need to determine it in the proper order of precedence: // resolver -> container -> default value const injectionMode = this.injectionMode || container.options.injectionMode || InjectionMode.PROXY; if (injectionMode !== InjectionMode.CLASSIC) { // If we have a custom injector, we need to wrap the cradle. const cradle = this.injector ? createInjectorProxy(container, this.injector) : container.cradle; // Return the target injected with the cradle return fn(cradle); } // We have dependencies so we need to resolve them manually if (dependencies.length > 0) { const resolve = this.injector ? wrapWithLocals(container, this.injector(container)) : container.resolve; const children = dependencies.map(p => resolve(p.name, { allowUnregistered: p.optional })); return fn(...children); } return fn(); }; } /** * Parses the dependencies from the given function. * If it's a class and has an extends clause, and no reported dependencies, attempt to parse it's super constructor. */ function parseDependencies(fn) { const result = parseParameterList(fn.toString()); if (result.length > 0) { return result; } const parent = Object.getPrototypeOf(fn); if (typeof parent === 'function' && parent !== Function.prototype) { // Try to parse the parent return parseDependencies(parent); } return result; } const camelCase = require('camel-case'); const nameFormatters = { camelCase }; /** * Given an array of glob strings, will call `require` * on them, and call their default exported function with the * container as the first parameter. * * @param {AwilixContainer} dependencies.container * The container to install loaded modules in. * * @param {Function} dependencies.listModules * The listModules function to use for listing modules. * * @param {Function} dependencies.require * The require function - it's a dependency because it makes testing easier. * * @param {String[]} globPatterns * The array of globs to use when loading modules. * * @param {Object} opts * Passed to `listModules`, e.g. `{ cwd: '...' }`. * * @param {(string, ModuleDescriptor) => string} opts.formatName * Used to format the name the module is registered with in the container. * * @return {Object} * Returns an object describing the result. */ function loadModules(dependencies, globPatterns, opts) { const container = dependencies.container; opts = optsWithDefaults(opts, container); const modules = dependencies.listModules(globPatterns, opts); const result = modules.map(m => { const items = []; const loaded = dependencies.require(m.path); // Meh, it happens. if (!loaded) { return items; } if (isFunction(loaded)) { // for module.exports = ... items.push({ name: m.name, path: m.path, value: loaded, opts: m.opts }); return items; } if (loaded.default && isFunction(loaded.default)) { // ES6 default export items.push({ name: m.name, path: m.path, value: loaded.default, opts: m.opts }); } // loop through non-default exports, but require the RESOLVER property set for // it to be a valid service module export. for (const key of Object.keys(loaded)) { if (key === 'default') { // default case handled separately due to its different name (file name) continue; } if (isFunction(loaded[key]) && RESOLVER in loaded[key]) { items.push({ name: key, path: m.path, value: loaded[key], opts: m.opts }); } } return items; }); result .reduce((acc, cur) => acc.concat(cur), []) .filter(x => x) .forEach(registerDescriptor.bind(null, container, opts)); return { loadedModules: modules }; } /** * Returns a new options object with defaults applied. */ function optsWithDefaults(opts, container) { return Object.assign({ // Does a somewhat-deep merge on the registration options. resolverOptions: Object.assign({ lifetime: Lifetime.TRANSIENT }, (opts && opts.resolverOptions)) }, opts); } /** * Given a module descriptor, reads it and registers it's value with the container. * * @param {AwilixContainer} container * @param {LoadModulesOptions} opts * @param {ModuleDescriptor} moduleDescriptor */ function registerDescriptor(container, opts, moduleDescriptor) { const inlineConfig = moduleDescriptor.value[RESOLVER]; let name = inlineConfig && inlineConfig.name; if (!name) { name = moduleDescriptor.name; let formatter = opts.formatName; if (formatter) { if (typeof formatter === 'string') { formatter = nameFormatters[formatter]; } if (formatter) { name = formatter(name, moduleDescriptor); } } } let moduleDescriptorOpts = moduleDescriptor.opts; if (typeof moduleDescriptorOpts === 'string') { moduleDescriptorOpts = { lifetime: moduleDescriptorOpts }; } const regOpts = Object.assign({}, opts.resolverOptions, moduleDescriptorOpts, inlineConfig); const reg = regOpts.register ? regOpts.register : isClass(moduleDescriptor.value) ? asClass : asFunction; container.register(name, reg(moduleDescriptor.value, regOpts)); } /** * Family tree symbol. * @type {Symbol} */ const FAMILY_TREE = Symbol('familyTree'); /** * Roll Up Registrations symbol. * @type {Symbol} */ const ROLL_UP_REGISTRATIONS = Symbol('rollUpRegistrations'); /** * Creates an Awilix container instance. * * @param {Function} options.require * The require function to use. Defaults to require. * * @param {string} options.injectionMode * The mode used by the container to resolve dependencies. Defaults to 'Proxy'. * * @return {object} * The container. */ function createContainer(options, parentContainer) { options = Object.assign({ injectionMode: InjectionMode.PROXY }, options); // The resolution stack is used to keep track // of what modules are being resolved, so when // an error occurs, we have something to present // to the poor developer who fucked up. let resolutionStack = []; // For performance reasons, we store // the rolled-up registrations when starting a resolve. let computedRegistrations = null; // Internal registration store for this container. const registrations = {}; /** * The `Proxy` that is passed to functions so they can resolve their dependencies without * knowing where they come from. I call it the "cradle" because * it is where registered things come to life at resolution-time. */ const cradle = new Proxy({ [inspect.custom]: inspectCradle }, { /** * The `get` handler is invoked whenever a get-call for `container.cradle.*` is made. * * @param {object} target * The proxy target. Irrelevant. * * @param {string} name * The property name. * * @return {*} * Whatever the resolve call returns. */ get: (target, name) => resolve(name), /** * Setting things on the cradle throws an error. * * @param {object} target * @param {string} name */ set: (_target, name, value) => { throw new Error(`Attempted setting property "${name}" on container cradle - this is not allowed.`); }, /** * Used for `Object.keys`. */ ownKeys() { return Array.from(cradle); }, /** * Used for `Object.keys`. */ getOwnPropertyDescriptor(target, key) { const regs = rollUpRegistrations(); if (Object.getOwnPropertyDescriptor(regs, key)) { return { enumerable: true, configurable: true }; } return undefined; } }); // The container being exposed. const container = { options, cradle: cradle, inspect: inspect$1, cache: new Map(), loadModules: loadModules$1, createScope, register: register, build, resolve, has, dispose, [inspect.custom]: inspect$1, // tslint:disable-next-line [ROLL_UP_REGISTRATIONS]: rollUpRegistrations, get registrations() { return rollUpRegistrations(); } }; // Track the family tree. const familyTree = parentContainer ? [container].concat(parentContainer[FAMILY_TREE]) : [container]; container[FAMILY_TREE] = familyTree; // We need a reference to the root container, // so we can retrieve and store singletons. const rootContainer = last(familyTree); return container; /** * Used by util.inspect (which is used by console.log). */ function inspect$1(depth, opts) { return `[AwilixContainer (${parentContainer ? 'scoped, ' : ''}registrations: ${Object.keys(container.registrations).length})]`; } /** * Rolls up registrations from the family tree. * This is cached until `bustCache` clears it. * * @param {boolean} bustCache * Forces a recomputation. * * @return {object} * The merged registrations object. */ function rollUpRegistrations(bustCache = false) { if (computedRegistrations && !bustCache) { return computedRegistrations; } computedRegistrations = Object.assign({}, (parentContainer && parentContainer[ROLL_UP_REGISTRATIONS](bustCache)), registrations); return computedRegistrations; } /** * Used for providing an iterator to the cradle. */ function* registrationNamesIterator() { const registrations = rollUpRegistrations(); for (const registrationName in registrations) { yield registrationName; } } /** * Creates a scoped container. * * @return {object} * The scoped container. */ function createScope() { return createContainer(options, container); } /** * Adds a registration for a resolver. */ function register(arg1, arg2) { const obj = nameValueToObject(arg1, arg2); const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)]; for (const key of keys) { const value = obj[key]; registrations[key] = value; } // Invalidates the computed registrations. computedRegistrations = null; return container; } /** * Returned to `util.inspect` when attempting to resolve * a custom inspector function on the cradle. */ function inspectCradle() { return '[AwilixContainer.cradle]'; } /** * Resolves the registration with the given name. * * @param {string | symbol} name * The name of the registration to resolve. * * @param {ResolveOptions} resolveOpts * The resolve options. * * @return {any} * Whatever was resolved. */ function resolve(name, resolveOpts) { resolveOpts = resolveOpts || {}; if (!resolutionStack.length) { // Root resolve busts the registration cache. rollUpRegistrations(true); } try { // Grab the registration by name. const resolver = computedRegistrations[name]; if (resolutionStack.indexOf(name) > -1) { throw new AwilixResolutionError(name, resolutionStack, 'Cyclic dependencies detected.'); } // Used in console.log. if (name === 'constructor') { return createContainer; } if (!resolver) { // The following checks ensure that console.log on the cradle does not // throw an error (issue #7). if (name === inspect.custom || name === 'inspect') { return inspectCradle; } // Edge case: Promise unwrapping will look for a "then" property and attempt to call it. // Return undefined so that we won't cause a resolution error. (issue #109) if (name === 'then') { return undefined; } // When using `Array.from` or spreading the cradle, this will // return the registration names. if (name === Symbol.iterator) { return registrationNamesIterator; } if (resolveOpts.allowUnregistered) { return undefined; } throw new AwilixResolutionError(name, resolutionStack); } // Pushes the currently-resolving module name onto the stack resolutionStack.push(name); // Do the thing let cached; let resolved; switch (resolver.lifetime || Lifetime.TRANSIENT) { case Lifetime.TRANSIENT: // Transient lifetime means resolve every time. resolved = resolver.resolve(container); break; case Lifetime.SINGLETON: // Singleton lifetime means cache at all times, regardless of scope. cached = rootContainer.cache.get(name); if (!cached) { resolved = resolver.resolve(container); rootContainer.cache.set(name, { resolver, value: resolved }); } else { resolved = cached.value; } break; case Lifetime.SCOPED: // Scoped lifetime means that the container // that resolves the registration also caches it. // When a registration is not found, we travel up // the family tree until we find one that is cached. cached = container.cache.get(name); if (cached !== undefined) { // We found one! resolved = cached.value; break; } // If we still have not found one, we need to resolve and cache it. resolved = resolver.resolve(container); container.cache.set(name, { resolver, value: resolved }); break; default: throw new AwilixResolutionError(name, resolutionStack, `Unknown lifetime "${resolver.lifetime}"`); } // Pop it from the stack again, ready for the next resolution resolutionStack.pop(); return resolved; } catch (err) { // When we get an error we need to reset the stack. resolutionStack = []; throw err; } } /** * Checks if the registration with the given name exists. * * @param {string | symbol} name * The name of the registration to resolve. * * @return {boolean} * Whether or not the registration exists. */ function has(name) { return name in rollUpRegistrations(); } /** * Given a registration, class or function, builds it up and returns it. * Does not cache it, this means that any lifetime configured in case of passing * a registration will not be used. * * @param {Resolver|Class|Function} targetOrResolver * @param {ResolverOptions} opts */ function build(targetOrResolver, opts) { if (targetOrResolver && targetOrResolver.resolve) { return targetOrResolver.resolve(container); } const funcName = 'build'; const paramName = 'targetOrResolver'; AwilixTypeError.assert(targetOrResolver, funcName, paramName, 'a registration, function or class', targetOrResolver); AwilixTypeError.assert(typeof targetOrResolver === 'function', funcName, paramName, 'a function or class', targetOrResolver); const resolver = isClass(targetOrResolver) ? asClass(targetOrResolver, opts) : asFunction(targetOrResolver, opts); return resolver.resolve(container); } /** * Binds `lib/loadModules` to this container, and provides * real implementations of it's dependencies. * * Additionally, any modules using the `dependsOn` API * will be resolved. * * @see lib/loadModules.js documentation. */ function loadModules$1(globPatterns, opts) { const _loadModulesDeps = { require: options.require || function (uri) { try { return require(uri); } catch (e) { console.log('failed to require file ', uri, e); throw e; } }, listModules, container }; loadModules(_loadModulesDeps, globPatterns, opts); return container; } /** * Disposes this container and it's children, calling the disposer * on all disposable registrations and clearing the cache. */ function dispose() { const entries = Array.from(container.cache.entries(