crave
Version:
Structure a node project your way with the ability to require models, controllers, or any file dynamically.
576 lines (486 loc) • 15.8 kB
JavaScript
// Require published node modules.
var path = require('path');
// Require local modules.
var Config = new (require(path.resolve(__dirname + '/config.js'))),
fs = require('fs'),
Log = require(path.resolve(__dirname + '/log.js')),
mkdirp = require('mkdirp');
// Local-Global variable instances.
var config,
cache = [],
debug,
log;
/* ************************************************** *
* ******************** Constructor
* ************************************************** */
/**
* Create a new instance of Crave. You can configure
* Crave by passing in an optional configuration object.
* @param config is a full or partial configuration object.
* @constructor
*/
var Crave = function(_config) {
handleCraveConfig(_config);
};
/* ************************************************** *
* ******************** Config
* ************************************************** */
/**
* Handle a change in the configuration object.
* @param config is the new configuration object.
*/
var handleCraveConfig = function(_config) {
if(config && config.cache.enable === true && _config && _config.cache && _config.cache.enable === false) {
clearCacheSync();
}
config = Config.setConfig(_config);
debug = (config.debug === true);
log = Config.getLog();
//log.t("Set Config: \n%s", JSON.stringify(config, undefined, 2));
return config;
};
/**
* Gets the current configuration object and returns it.
*/
var getConfig = function() {
return config;
};
/* ************************************************** *
* ******************** Cache
* ************************************************** */
/**
* Retrieve the cached json data from the file specified by the config object.
* An error and/or cache is returned to the callback.
* @param cb is a callback method where the result and/or error is returned.
*/
var loadCache = function(_config, cb) {
if( ! _config.cache || ! _config.cache.enable) {
log.t("Ignoring cache found only in memory with the value of: \n%s\n", JSON.stringify(cache, undefined, 2));
return cb(); //return cb(undefined, cache);
}
if(! _config.cache.path) {
return cb(new Error("Invalid cache path."));
}
if( ! fs.existsSync(_config.cache.path)) {
log.t("Cache not yet saved to disk, loading from memory with value of: \n%s\n", JSON.stringify(cache, undefined, 2));
return cb(undefined, cache);
}
fs.readFile(_config.cache.path, 'utf8', function(err, data){
if(err) {
return cb(err);
}
try {
_cache = JSON.parse(data);
} catch(err) {
return cb(err);
}
log.t("Cache loaded from disk with value of: \n%s\n", JSON.stringify(_cache, undefined, 2));
return cb(err, _cache, true);
});
};
/**
* Updates the global cache value and persist the cache to disk if the
* caching option is enabled in the config. An error and/or cache is
* returned to the callback.
* @param _cache is the json cache object to be persisted.
* @param cb is a callback method where the result and/or error is returned.
*/
var saveCache = function(_cache, cb) {
cache = _cache;
if( ! config.cache || ! config.cache.enable) {
log.t("Cache updated in memory with value of: \n%s\n", JSON.stringify(_cache, undefined, 2));
return cb(undefined, _cache)
}
if(! config.cache.path) {
return cb(new Error("Invalid cache path."));
}
mkdirp(config.cache.path.substring(0, config.cache.path.lastIndexOf(path.sep)), function(err) {
if(err) {
return cb(err);
}
fs.writeFile(config.cache.path, JSON.stringify(_cache, undefined, 2), function(err) {
if(err) {
return cb(err);
}
log.t("Cache saved to disk with value of: \n%s\n", JSON.stringify(_cache, undefined, 2));
cb(undefined, _cache, true);
});
});
};
var removeTrailingSlash = function(v) {
if(v && v.length > 0 && v.substring(v.length -1) === path.sep) {
v = v.substring(0, v.length -1);
}
return v;
};
var clearCacheSync = function() {
cache = [];
if(config.cache && config.cache.path) {
if(fs.existsSync(config.cache.path)) {
fs.unlinkSync(config.cache.path);
log.t("Cache cleared and '%s' deleted.", config.cache.path);
} else {
log.t("Cache cleared.");
}
} else {
log.t("Cache cleared.");
}
return true;
};
/**
* Asynchronously clear the current cache in memory and on
* disk if it exists. If the cache file could not be deleted
* an error will be returned in the callback.
*/
var clearCache = function(cb) {
if( ! cb) {
cb = function(err) {
if(err) {
log.e(err);
}
};
}
cache = [];
if(config.cache && config.cache.path) {
fs.exists(config.cache.path, function(v) {
if(v) {
fs.unlink(config.cache.path, function(err) {
if( ! err) {
log.t("Cache cleared and '%s' deleted.", config.cache.path);
}
cb(err);
});
} else {
log.t("Cache cleared");
cb();
}
});
} else {
log.t("Cache cleared");
cb();
}
};
/**
* Update a specific directory's list of files in the cache.
* @param _cache is the cache object to update.
* @param directory is the directory to update.
* @param files is an ordered array of file paths to require.
* @param cb is a callback method where a result and/or error are returned.
*/
var updateDirectoryInCache = function(_cache, directory, files, cb) {
log.t("Update Directory In Cache: \nDirectory: %s \nFiles: %s\n", directory, JSON.stringify(files, undefined, 2));
for(var i = 0; i < _cache.length; i++) {
if(_cache[i]["directory"] === directory) {
_cache[i]["files"] = files;
return saveCache(_cache, cb);
}
}
_cache.push({
directory: directory,
files: files
});
saveCache(_cache, cb);
};
/**
* Create a list of to require from the cache.
* @param _cache is the current cache.
* @param cb is a callback method where the list of files, or error, is returned.
*/
var createListFromCache = function(_cache, cb) {
var list = [];
try {
for(var y = 0; y < _cache.length; y++) {
for(var x = 0; x < _cache[y]["files"].length; x++) {
list.push(_cache[y].files[x]);
}
}
} catch(err) {
log.e(err);
log.e("Cannot create a list of files from an undefined cache. Returning a blank list of files.");
list = [];
//TODO: Return error here or fail silently?
}
cb(undefined, list);
};
/* ************************************************** *
* ******************** Private Methods
* ************************************************** */
/**
* Find all the files in a given directory and its subdirectories. Perform an action
* on all the files by calling the action parameter method for each file.
* @param {String} directory is the absolute path to the root directory.
* @param {Function} action is a method to be called for each file.
* @param {Function} cb is a callback method where any results or errors will be returned to.
*/
var walkAsync = function(directory, action, cb) {
if( ! directory) {
return cb(new Error("Invalid directory value of '" + directory + "'"));
}
// Ensure the directory does not have a trailing slash.
if(directory.substring(directory.length -1) === path.sep) {
directory = directory.substring(0, directory.length -1);
}
// Get a list of all the files and folders in the directory.
fs.readdir(directory, function(err, list) {
if(err) {
return cb(err, false);
}
// Create a count of the number of files and/or folders in the directory.
var pending = list.length;
// If we are at the end of the list, then return success!
if( ! pending) {
return cb(null, true);
}
// For each item in the list, perform an "action" and continue on.
list.forEach(function(file) {
// Check if the file is invalid; ignore invalid files.
if(isFileInvalid(file)) {
log.d("\tSkipping: " + directory + path.sep + file);
pending--;
return; // Not entirely sure why this works lol but I'm tired, TODO: see why "return cb(null, true);"" broke the codes.
}
// Add a trailing / and file to the directory we are in.
file = directory + path.sep + file;
// Check if the item is a file or directory.
fs.stat(file, function(err, stat) {
if(err) {
return cb(err, false);
}
// If a directory, add it to our list and continue walking.
if (stat && stat.isDirectory()) {
walkAsync(file, action, function(err, success) {
if ( ! --pending) {
cb(null, success);
}
});
// If a file, perform the action on the file and keep walking.
} else {
action(file, function(err) {
if(err) {
cb(err, false);
}
if (!--pending) {
cb(null, true);
}
});
}
});
});
});
};
/**
* Checks if the file should be required or if the
* folder should be searched for files to require.
* This is a helper function of WalkAsync.
* @param {String} file is a file name to be validated.
*/
var isFileInvalid = function(file) {
// List of invalid file and folder names. Names must be in lower case.
var invalidFiles = ["node_modules"];
// Skip all hidden files, aka those that begin with a .
if(file.substring(0,1) === ".") {
return true;
}
// Skip all files and folders in the invalid file array.
for(var i = 0; i < invalidFiles.length; i++) {
if(file.toLowerCase().indexOf(invalidFiles[i]) != -1) {
return true;
}
}
return false;
};
var buildDirectoryList = function(directory, types, cb) {
log.t("Build Directory List: \n\tDirectory:\t%s\n\tTypes: \t\t[ %s ]\n", directory, types);
var lists = [];
for(var i = 0; i < types.length; i++) {
lists[i] = [];
};
walkAsync(directory, function(file, cb) {
log.t("Checking File: %s", file);
switch(config.identification.type) {
default:
case 'string':
fs.readFile(file, 'utf8', function(err, data) {
// Check if the file contains a route tag.
for(var i = 0; i < types.length; i++) {
// If it contains a route tag, then add it to the list of files to require.
if(data.toLowerCase().indexOf(config.identification.identifier + " " + types[i]) != -1) {
lists[i].push(file);
return cb();
}
}
log.t("\t\tSkipping File: %s", file);
return cb();
});
break;
case 'filename':
var fileName = file.substring(file.lastIndexOf(path.sep)+1).toLowerCase();
for(var i = 0; i < types.length; i++) {
if(fileName.indexOf(config.identification.identifier + types[i]) != -1) {
lists[i].push(file);
return cb();
}
}
log.t("\tSkipping File: %s", file);
return cb();
break;
}
}, function(err, success) {
if(err) {
return cb(err);
}
var list = [];
for(var y = 0; y < lists.length; y++) {
for(var x = 0; x < lists[y].length; x++) {
list.push(lists[y][x]);
}
}
log.d("Directory list built successfully: \n%s\n", JSON.stringify(list, undefined, 2));
updateDirectoryInCache(cache, directory, list, cb);
});
};
/**
* Attempt to require an ordered list of files and return a list
* of files that were successfully required.
* @param list is an ordered array of absolute paths to files to be required.
* @param cb is a callback method where the result and/or error will be returned.
*/
var requireFiles = function(_list, _cb) {
log.t("Require Files: \n%s\n", JSON.stringify(_list, undefined, 2));
var list = _list,
cb = _cb;
var _arguments = arguments;
var _this = this;
var returnValues = [];
var length = Object.keys(_arguments).length;
for(var i = 2; i < length; i++) {
if(_arguments[i]) {
_arguments[i-2] = _arguments[i];
delete _arguments[i];
}
}
for (var i = 0; i < list.length; i++) {
// Don't try to load a file you can't.
if (list[i] === undefined || list[i] === '') {
log.e("Can't require a file when the location is undefined.");
list.splice(i, 1);
i--;
continue;
}
if (!fs.existsSync(list[i])) {
log.e("File at location "+list[i]+" does not exist.");
list.splice(i, 1);
i--;
continue;
}
log.d("\tRequire: " + list[i]);
var value;
try {
value = require(list[i]).apply(_this, _arguments);
} catch(err) {
value = {
error: list[i] + " contains an error an therefore could not be required by Crave.",
stack: err.stack
};
log.e(value.error);
log.e(value.stack);
}
returnValues.push(value);
}
if(cb !== undefined && cb !== null) {
cb(undefined, list, returnValues);
}
};
/**
* Build or load a cache, then require all the files
* in the correct order.
* If an error occurred it will be passed to the callback.
*/
var loadDirectory = function(_directory, _types, _cb) {
var cb = _cb,
directory = _directory,
types = _types;
var _arguments = arguments;
var _this = this;
var length = Object.keys(_arguments).length
for(var i = 3; i < length; i++) {
if(_arguments[i]) {
_arguments[i-1] = _arguments[i];
delete _arguments[i];
}
}
loadCache(config, function(err, _cache) {
if(err) {
cb(err);
} else if(_cache && _cache[0] && _cache[0]["directory"]) {
createListFromCache(_cache[0], function (err, files) {
if(err) {
cb(err);
} else {
_arguments[0] = files;
_arguments[1] = cb;
requireFiles.apply(null, _arguments);
}
});
} else {
buildDirectoryList(directory, types, function (err, _cache) {
if(err) {
cb(err);
} else {
createListFromCache(_cache, function (err, files) {
if(err) {
cb(err);
} else {
_arguments[0] = files;
_arguments[1] = cb;
requireFiles.apply(null, _arguments);
}
});
}
});
}
});
};
/**
* Build or load a cache using multiple possible file locations,
* then require all the files in the correct order.
* If an error occurred it will be passed to the callback.
*/
//var loadDirectories = function() {
//TODO: Implement this.
//};
/**
* Load a file by requiring it and passing in all the arguments.
*/
/*var requireFile = function(_file, _cb) {
var cb = _cb,
file = _file;
var _arguments = arguments;
var _this = this;
for(var i = 2; i < Object.keys(_arguments).length; i++) {
if(_arguments[i]) {
_arguments[i-2] = _arguments[i];
delete _arguments[i];
}
}
require(file).apply(_this, _arguments);
cb(undefined, [ file ]);
}; */
/* ************************************************** *
* ******************** Public API
* ************************************************** */
Crave.prototype.directory = loadDirectory;
//Crave.prototype.directories = loadDirectory;
Crave.prototype.files = requireFiles;
//Crave.prototype.file = requireFile;
Crave.prototype.setConfig = handleCraveConfig;
Crave.prototype.getConfig = getConfig;
Crave.prototype.clearCache = clearCache;
Crave.prototype.clearCacheSync = clearCacheSync;
// These are exposed for testing purposes only.
if(process.env.NODE_ENV && process.env.NODE_ENV.toLowerCase() === "test") {
Crave.prototype.loadCache = loadCache;
Crave.prototype.updateDirectoryInCache = updateDirectoryInCache;
}
exports = module.exports = new Crave();
exports = Crave;