src/ModuleResolver.js

Summary

Maintainability
A
1 hr
Test Coverage
const path = require('path');
const fs = require('fs');

/**
 * The module resolver will resolve modules and converts their inject information into
 * a set of information, the objectManager needs in order to handle injection properly
 *
 * @author Wolfgang Felbermeier (@f3lang)
 */
class ModuleResolver {

    /**
     * @param moduleSrc An array with absolute paths to folders including
     * modules to use for injection
     * @param cacheFile A path to a json file, that should be used as a cache.
     * @param moduleAnalyzer
     */
    constructor(moduleSrc, cacheFile, moduleAnalyzer) {
        this.cache = {
            moduleMap: {},
            fileCache: {}
        };
        if (cacheFile) {
            this.cacheFile = cacheFile;
            if (fs.existsSync(cacheFile)) {
                this.cache = require(cacheFile);
            }
        }
        this.moduleAnalyzer = moduleAnalyzer;
        this.nameCollisionMap = [];

        let i = moduleSrc.length;
        while (i--) {
            this.addModuleSrcDir(moduleSrc[i]);
        }
        if (cacheFile) {
            this.writeCache();
        }
    }

    /**
     * Add an additional source directory to the Module Resolver
     * @param {string} dir The directory to parse
     */
    addModuleSrcDir(dir) {
        let modulePaths = fs.readdirSync(dir);
        modulePaths.forEach(src => {
            let stat = fs.statSync(path.join(dir, src));
            if (stat.isDirectory()) {
                this.addModuleSrcDir(path.join(dir, src));
            } else {
                if (path.extname(src).toLowerCase() === '.js') {
                    this.processModule(path.join(dir, src), stat);
                }
            }
        });
        if (this.cacheFile) {
            this.writeCache();
        }
    }

    /**
     * Fetches the module and handles caching.
     * @param modulePath The absolute path to the module source file
     * @param stat The stat of the module file
     */
    processModule(modulePath, stat) {
        if (this.cacheFile) {
            if (this.cache.fileCache[path.basename(modulePath)]) {
                let stat = fs.statSync(modulePath);
                let modificationTime = stat.mtimeMs || stat.mtime * 1;
                if (this.cache.fileCache[path.basename(modulePath)].mtimeMs < modificationTime) {
                    this.cache.fileCache[path.basename(modulePath)].mtimeMs = modificationTime;
                    this.fetchModule(modulePath);
                }
            } else {
                let stat = fs.statSync(modulePath);
                this.cache.fileCache[path.basename(modulePath)] = {
                    mtimeMs: stat.mtimeMs || stat.mtime * 1
                };
                this.fetchModule(modulePath);
            }
        } else {
            this.fetchModule(modulePath);
        }
    }

    /**
     * Fetches the module information and puts them into the cache.
     * @param modulePath The path to the module. Has to be resolvable by require()
     * @return {Promise<any>}
     */
    fetchModule(modulePath) {
        let module = require(modulePath);
        let moduleConfig = this.moduleAnalyzer.analyze(module);
        let moduleNames = Object.getOwnPropertyNames(module).indexOf('alias') > -1 ? module.alias : [];
        moduleNames.push(module.name);
        let i = moduleNames.length;
        while (i--) {
            if (this.nameCollisionMap.indexOf(moduleNames[i]) > -1) {
                throw new Error("module name " + moduleNames[i] + " is already in use");
            }
        }
        moduleNames.forEach(name => {
            this.nameCollisionMap.push(name);
            this.cache.moduleMap[name] = {
                path: modulePath,
                config: moduleConfig,
                name: name,
                originModuleName: module.name
            }
        });
    }

    /**
     * Get the complete set of resolved modules
     * @return {{moduleMap: {}, fileCache: {}}|*}
     */
    getResolvedModules() {
        return this.cache;
    }

    /**
     * Write the cache file.
     */
    writeCache() {
        fs.writeFileSync(this.cacheFile, JSON.stringify(this.cache, null, '\t'));
    }

    /**
     * Retrieves a list of module names which match a regular expression
     * @param {RegExp} regex
     * @return {Array} An array of matched module names
     */
    getModuleNames(regex) {
        return Object.keys(this.cache.moduleMap).filter(name => regex.test(name));
    }

}

module.exports = ModuleResolver;
module.exports.inject = ['config:cdi:moduleSrc', 'config:cdi:resolver.cacheFile', 'ModuleAnalyzer'];