resolver.js

import Symbol from 'es6-symbol';
import isNil from 'is-nil';
import isString from 'is-string';
import isFunction from 'is-function';
import isArray from 'is-array';
import forEach from 'for-each';
import toposort from 'toposort';
import { requires, assert } from './utils/assertions';

const DEFAULT_DEPENDENCIES = [];
const CIRC_REF = 'Circular dependency is detected';
const INVALID_NAMESPACE_TYPE = 'Invalid namespace name type, expected "string", but got';
const INVALID_PATH_TYPE = 'Invalid path type, expected "string", but got';

const FIELDS = {
    storage: Symbol('storage')
};

/**
 * Represents a dependency resolver.
 */
class Resolver {

    /**
     * Creates a new instance of Namespace.
     * @param storage - Module's storage.
     */
    constructor(storage) {
        requires(storage, 'storage');

        this[FIELDS.storage] = storage;
    }

    /**
     * Resolves a module by given full path.
     * @param {string} fullPath - Full path of a module.
     * @returns {any} Value of resolved module.
     */
    resolve(fullPath) {
        requires(fullPath, 'path');
        assert(isString(fullPath), `${INVALID_PATH_TYPE} "${typeof fullPath}"`);

        const graph = [];
        const chain = [];
        const checkCircularDependency = (targetPath, dependencies) => {
            forEach(dependencies, i => graph.push([targetPath, i]));
            chain.push(...dependencies);

            try {
                toposort(graph);
            } catch (e) {
                throw new ReferenceError(`${CIRC_REF}: ${fullPath} -> ${chain.join(' -> ')}`);
            }
        };
        const resolveModule = (targetPath) => {
            const module = this[FIELDS.storage].getItem(targetPath);

            if (isNil(module)) {
                return null;
            }

            if (module.isInitialized()) {
                return module.getValue();
            }

            const resolveDependencies = (dependencies) => {
                if (isArray(dependencies)) {
                    checkCircularDependency(targetPath, dependencies);
                    return dependencies.map((currentPath) => {
                        if (isString(currentPath)) {
                            return resolveModule(currentPath);
                        } else if (isFunction(currentPath)) {
                            return currentPath();
                        }

                        return undefined;
                    });
                }

                if (isFunction(dependencies)) {
                    const result = dependencies();

                    if (!isArray(result)) {
                        return [result];
                    }

                    return result;
                }

                return DEFAULT_DEPENDENCIES;
            };

            module.initialize(resolveDependencies(module.getDependencies()));
            return module.getValue();
        };

        return resolveModule(fullPath);
    }

    /**
     * Resolves all modules by a given namespace name.
     * @param {string} namespace - Target namespace name.
     * @param {boolean} [nested=false] - Value that detects whether it needs to resolve modules from nested namespaces.
     * If 'true', all resolved values will be put into an array.
     * @returns {Map<string, (any|Array<any>)>} Map of module values, where key is a module name.
     */
    resolveAll(namespace, nested = false) {
        assert(isString(namespace), `${INVALID_NAMESPACE_TYPE} "${typeof namespace}"`);

        const result = {};
        let namespaces = [namespace];

        if (nested) {
            namespaces = namespaces.concat(this[FIELDS.storage].namespaces(namespace));
        }

        forEach(namespaces, (currentNamespace) => {
            this[FIELDS.storage].forEachIn(currentNamespace, (module, path) => {
                const name = module.getName();

                if (!nested) {
                    result[name] = this.resolve(path);
                } else {
                    if (!result[name]) {
                        result[name] = [];
                    }

                    result[name].push(this.resolve(path));
                }
            });
        });

        return result;
    }
}

export default Resolver;