UNPKG

@shahen.poghosyan/awilix

Version:

Extremely powerful dependency injection container.

363 lines 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const util = require("util"); const list_modules_1 = require("./list-modules"); const load_modules_1 = require("./load-modules"); const resolvers_1 = require("./resolvers"); const utils_1 = require("./utils"); const injection_mode_1 = require("./injection-mode"); const lifetime_1 = require("./lifetime"); const errors_1 = require("./errors"); /** * 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: injection_mode_1.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({ [util.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, cache: new Map(), loadModules, createScope, register: register, build, resolve, has, dispose, [util.inspect.custom]: inspect, // 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 = utils_1.last(familyTree); return container; /** * Used by util.inspect (which is used by console.log). */ function inspect(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 = utils_1.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 errors_1.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 === util.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 errors_1.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_1.Lifetime.TRANSIENT) { case lifetime_1.Lifetime.TRANSIENT: // Transient lifetime means resolve every time. resolved = resolver.resolve(container); break; case lifetime_1.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_1.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 errors_1.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'; errors_1.AwilixTypeError.assert(targetOrResolver, funcName, paramName, 'a registration, function or class', targetOrResolver); errors_1.AwilixTypeError.assert(typeof targetOrResolver === 'function', funcName, paramName, 'a function or class', targetOrResolver); const resolver = utils_1.isClass(targetOrResolver) ? resolvers_1.asClass(targetOrResolver, opts) : resolvers_1.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(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: list_modules_1.listModules, container }; load_modules_1.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()); container.cache.clear(); return Promise.all(entries.map(([name, entry]) => { const { resolver, value } = entry; const disposable = resolver; if (disposable.dispose) { return Promise.resolve().then(() => disposable.dispose(value)); } return Promise.resolve(); })).then(() => undefined); } } exports.createContainer = createContainer; //# sourceMappingURL=container.js.map