autowire
Version:
Automagically autowire everything
429 lines (357 loc) • 11.8 kB
JavaScript
var PATH = require("path");
var Promise = require("bluebird");
var _ = require("lodash");
var fs = require("fs");
var log = require("./DebugLogger.js").getLogger().withParentPrefix().withColor(4);
var ClassHelper = require("./helpers/ClassHelper.js");
function ModuleFinder() {
this.currentPath = "";
this.moduleCache = {};
this.notFoundCache = {};
this.alias = {};
this.wiredClasses = {};
this.markedAsClass = {};
// module name to absolute module root file path mapping
this.nameCache = {};
this.projectRoot = undefined;
}
ModuleFinder.prototype.addAlias = function(alias, realname) {
this.alias[alias] = realname;
return this;
};
ClassHelper.attachClone(ModuleFinder);
ModuleFinder.prototype.setCurrentPath = function(currentPath) {
var self = this;
log.trace("setCurrentPath", currentPath);
self.currentPath = currentPath;
self._currentName = PATH.parse(self.currentPath).base;
return self;
};
ModuleFinder.prototype.setParentModuleName = function(parentModuleName) {
this.parentModuleName = parentModuleName;
};
/**
* Alias name to realname.
*
* e.g. addAlias("_", "lodash")
*
* @param alias
* @param realname
*/
ModuleFinder.prototype.addAlias = function(alias, realname) {
log("Aliasing \"%s\" = \"%s\"", alias, realname);
this.alias[alias] = realname;
};
/**
* Attach object to name, so it is saved in cache
* and returned when discovery is in place.
*
* @param name
* @param object
*/
ModuleFinder.prototype.wire = function(name, object) {
this.moduleCache[name] = object;
};
ModuleFinder.prototype.addToCache = function(paramName, _module) {
log.trace("Adding to cache \"%s\"", paramName);
this.moduleCache[paramName] = _module;
};
ModuleFinder.prototype.markAsNotFound = function(paramName) {
this.notFoundCache[paramName] = true;
};
ModuleFinder.prototype.invalidateCache = function() {
this.nameCache = {};
this.moduleCache = {};
this.notFoundCache = {};
};
//ModuleFinder.prototype.maybeInstantiate = function(clazz, name) {
// var self = this;
//
// if(clazz && _.isFunction(clazz)) {
// var instantiate = _.get(clazz, "autowire.instantiate");
// if(! instantiate) {
// self.addToCache(name, clazz);
// return clazz;
// } else {
// log("[%s] instantiating %s", this.parentModuleName, clazz.name);
// var instance = self.instantiateClass(clazz);
//
// var singleton = _.get(clazz, "autowire.singleton");
// if(singleton) {
// log("[%s] saving as singleton %s", this.parentModuleName, clazz.name);
// self.addToCache(name, instance);
// }
// return instance;
// }
// } else {
// self.addToCache(name, clazz);
// return clazz;
// }
//};
//ModuleFinder.prototype.instantiateClass = function(clazz) {
// var obj = Object.create(clazz["prototype"]);
// this.injector.applyMap(clazz, obj);
// return obj;
//};
ModuleFinder.prototype.findWiredClass = function(clazz, className) {
return _.find(this.wiredClasses, function(c){
return c.wiredClasses === clazz || c.className === className;
});
};
/**
* Extend impot paths based on current context. Import up the directory tree until package.json is found.
*
* @param importPaths
*/
ModuleFinder.prototype.getExtendedImportPaths = function(importPaths) {
var _importPaths = _.clone(importPaths);
var i = 10; // MAX_DEPTH
var path = "./";
var lastRealPath = -1;
while(i--) {
path += "../";
var realPath = PATH.resolve(path)+PATH.sep;
if(fs.existsSync(realPath+"package.json")) {
_importPaths.push(realPath);
break;
}
if(lastRealPath === realPath) {
break;
}
_importPaths.push(realPath);
lastRealPath = realPath;
}
return _importPaths;
};
/**
* Attempt to auto discover module by name.
*
* @param paramName by which we seek module
* @returns imported module or undefined
*/
ModuleFinder.prototype.discoverModuleByName = function(paramName) {
var self = this;
// compute alias
var alias = this.alias[paramName];
if(alias) {
if(!_.isString(alias)) {
throw new Error("Alias must be a string alias = \""+JSON.stringify(alias)+"\"");
}
log.debug("[%s] Discover module \"%s\" (alias of \"%s\")", self.parentModuleName, paramName, alias);
paramName = alias;
} else {
log.debug("[%s] Discover module \"%s\"", self.parentModuleName, paramName);
}
// try cache
if(this.notFoundCache[paramName]) {
log.trace("[%s] Marked as not found \"%s\"", self.parentModuleName, paramName);
return _return(undefined);
}
if(this.moduleCache[paramName]) {
log.trace("[%s] Found in cache \"%s\"", self.parentModuleName, paramName);
return _return(this.moduleCache[paramName]);
}
var ret = undefined;
// try node_modules
if(tryImport(paramName)) {
//return self.maybeInstantiate(ret, paramName);
return _return(ret);
}
// try stored name
var absPath = self.nameCache[paramName];
if(absPath) {
log.trace("[%s] Found in name cache \"%s\" at \"%s\"", self.parentModuleName, paramName, absPath);
if(tryImport(absPath)) {
//return self.maybeInstantiate(ret, paramName);
return _return(ret);
}
}
// try sub module
var _paramName = paramName.replace(/\$/g, '/');
if(_paramName.indexOf('/') !== -1) {
log.trace("[%s] Resolving submodule \"%s\"", self.parentModuleName, paramName)
var ps = _paramName.split('/');
var p = ps.shift();
var subPath = ps.join('/');
absPath = self.nameCache[p];
log(`[${self.parentModuleName}] absPath = self.nameCache['${p}'];`);
log(`[${self.parentModuleName}] absPath = ${absPath};`);
if(absPath) {
log.trace("[%s] Trying as sub module of \"%s\"", self.parentModuleName, p);
log(`absPath = ${absPath}`);
log(`subPath = ${subPath}`);
var subAbsPath = PATH.resolve(PATH.parse(absPath).dir+PATH.sep+subPath);
if(tryImport(subAbsPath)) {
return _return(ret);
}
}
}
log.trace("[%s] Module not found \"%s\"", self.parentModuleName, paramName);
// mark as not found
this.notFoundCache[paramName] = true;
return _return(ret);
function _return(ret) {
if(! _.isUndefined(ret)) {
log("[%s] Returning found \"%s\"", self.parentModuleName, paramName);
} else {
log("[%s] Returning undefined, module \"%s\" not found", self.parentModuleName, paramName);
}
return ret;
}
function tryImport(name) {
try {
log.trace("[%s] Trying \"%s\"", self.parentModuleName, name);
var lib = require(name);
log("[%s] Found lib: \"%s\" resolved to \"%s\"", self.parentModuleName, paramName, name);
ret = lib;
return true;
} catch(ex) {
//if(! ex.message.match(new RegExp("Cannot find module "))) {
if(! ex.message.match(new RegExp("Cannot find module '"+ _.escapeRegExp(name)+"'"))) {
exit = true;
log.error("[%s] Error while discovering for \"%s\"", self.parentModuleName, paramName, ex);
throw ex;
}
//else {
// log("[%s] marking as not found \"%s\"", self.parentModuleName, name);
// self.notFoundCache[name] = true;
//}
return false;
}
}
};
/**
* This is global, cross npm module object
*/
ModuleFinder.globalNameCache = {};
ModuleFinder.globalModuleCache = {};
ModuleFinder.globalAliases = {};
ModuleFinder.prototype.updateNameCache = function(name, absPath) {
var self = this;
/**
* Don't store this names
*/
var blackList = {
"package": true
};
name = nameMapper(name);
if(self.nameCache[name]) {
log.error("[%s] Conflicting names \"%s\" at paths: \"%s\" and \"%s\")",
self.parentModuleName, name, absPath, self.nameCache[name]);
}
if(blackList[name]) {
log("[%s] Blacklisted \"%s\" at path \"%s\"", self.parentModuleName, name, absPath);
} else {
log("[%s] Saving %s -> %s", self.parentModuleName, name, absPath);
self.nameCache[name] = absPath;
}
return self;
function nameMapper(name) {
// to camelCase
if(name.indexOf('-') !== -1) {
return name.split('-').map(function(str, i) {
return (i === 0 ? str[0] : str[0].toUpperCase()) + str.substring(1);
}).join('');
}
return name;
}
};
ModuleFinder.prototype.generateNameCache = function() {
var self = this;
var list = fs.readdirSync(self.currentPath);
var rootPath = findProjectRoot(self.currentPath);
if(! rootPath) {
throw new Error("Cannot find project root path from currentPath = \""+self.currentPath+"\"");
}
self.projectRoot = rootPath;
log("[%s] Project root \"%s\"", self.parentModuleName, rootPath);
if(ModuleFinder.globalNameCache[rootPath]) {
log("[%s] Found cached nameCache for current project root", self.parentModuleName);
self.nameCache = ModuleFinder.globalNameCache[rootPath];
self.moduleCache = ModuleFinder.globalModuleCache[rootPath];
self.alias = ModuleFinder.globalAliases[rootPath];
return ModuleFinder.globalNameCache[rootPath];
}
self.nameCache = ModuleFinder.globalNameCache[rootPath] = {};
self.moduleCache = ModuleFinder.globalModuleCache[rootPath] = {};
self.alias = ModuleFinder.globalAliases[rootPath] = {
'_': 'lodash'
};
mapAllNames(rootPath);
mapNodeModules(rootPath);
/**
* Saves absPath -> filename mapping in the map
* @param path
*/
function mapAllNames(path) {
log("[%s] Mapping %s", self.parentModuleName, path);
fs.readdirSync(path).forEach(function(p){
var absPath = PATH.resolve(path+PATH.sep+p);
var lstat = fs.lstatSync(absPath);
if(lstat.isFile() && isModule(p)) {
var name = getModuleName(p);
self.updateNameCache(name, absPath);
} else if(! isIgnoreFolder(p) && lstat.isDirectory()) {
mapAllNames(absPath);
}
});
function isIgnoreFolder(folderName) {
return !folderName || folderName === 'node_modules' || folderName[0] === '.';
}
function isModule(p) {
return p.match(/\.(js|json)$/);
}
function getModuleName(p) {
return p.replace(/\.(js|json)$/, '');
}
}
function mapNodeModules(rootPath) {
var nodeModulesPath = rootPath+PATH.sep+"node_modules";
if(fs.existsSync(nodeModulesPath)) {
log("[%s] Mapping node_modules at \"%s\"", self.parentModuleName, nodeModulesPath);
fs.readdirSync(nodeModulesPath).forEach(function(dir){
var dirAbsPath = nodeModulesPath+PATH.sep+dir;
if(fs.lstatSync(dirAbsPath).isDirectory()) {
var packageJsonPath = dirAbsPath+PATH.sep+"package.json";
if(fs.existsSync(packageJsonPath)) {
var packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
var moduleName = packageJson.name;
var moduleMain = packageJson.main;
if(moduleName && moduleMain) {
var absPath = PATH.resolve(dirAbsPath+PATH.sep+moduleMain);
self.updateNameCache(moduleName, absPath);
}
}
}
});
}
}
/**
* Going up the folder tree returns the first
* folder with package.json present.
*
* @param startPath
* @returns {*}
*/
function findProjectRoot(startPath) {
log.trace("findProjectRoot startPath = %s", startPath);
if(isDiskRoot()) {
if(hasPackageJson()) {
return startPath;
} else {
throw new Error("Cannot find project root");
}
} else if(hasPackageJson()) {
return startPath;
} else {
return findProjectRoot(PATH.resolve(startPath+PATH.sep+".."));
}
function hasPackageJson() {
return fs.existsSync(startPath+PATH.sep+"package.json");
}
function isDiskRoot() {
return startPath.match(/^([A-Z]:\\\\|\/)$/);
}
}
};
module.exports = ModuleFinder;