Initial commit
This commit is contained in:
538
lib/modular.js
Normal file
538
lib/modular.js
Normal file
@@ -0,0 +1,538 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var SyncEventEmitter = require(path.join(__dirname, 'synceventemitter.js'));
|
||||
|
||||
function Modular (options) {
|
||||
SyncEventEmitter.call(this);
|
||||
|
||||
var settings;
|
||||
var modules = new Map(); //Map fullName => Module
|
||||
var instance = this;
|
||||
var watchers;
|
||||
var watchedFiles;
|
||||
var autoloadModules = [];
|
||||
|
||||
process.chdir(path.dirname(module.parent.filename));
|
||||
|
||||
//Default options if not provided
|
||||
settings = options || {};
|
||||
|
||||
watchedFiles = new Map();
|
||||
watchers = new Map();
|
||||
settings.restartOnChange = settings.restartOnChange === true;
|
||||
|
||||
if (settings.files === undefined) {
|
||||
settings.files = [];
|
||||
}
|
||||
|
||||
if (settings.exclude === undefined) {
|
||||
settings.exclude = [module.parent.filename];
|
||||
}
|
||||
|
||||
if (settings.basePath === undefined) {
|
||||
settings.basePath = path.dirname(module.parent.filename);
|
||||
}
|
||||
|
||||
if (settings.logLevel === undefined) {
|
||||
settings.logLevel = 0;
|
||||
}
|
||||
|
||||
/* process.on('uncaughtException', function (err) {
|
||||
log ('error', 'Unhandled exception', err);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', function(reason, p) {
|
||||
log('error', 'Unhandled rejection at: Promise ', p, ' reason: ', reason);
|
||||
});
|
||||
*/
|
||||
|
||||
var applicationLoaded = false;
|
||||
|
||||
//Start the application
|
||||
this.start = function () {
|
||||
//Load application files
|
||||
if (!applicationLoaded && settings.files && settings.files !== undefined) {
|
||||
if (!Array.isArray(settings.files)) {
|
||||
settings.files = [ settings.files ];
|
||||
}
|
||||
|
||||
for (let file of settings.files) {
|
||||
if (file instanceof Object) {
|
||||
load (file);
|
||||
}
|
||||
else {
|
||||
load ({ path : file });
|
||||
}
|
||||
}
|
||||
|
||||
applicationLoaded = true;
|
||||
}
|
||||
|
||||
try {
|
||||
//Load modules tagged as autoload
|
||||
for (var mod of autoloadModules) {
|
||||
loadModule(mod);
|
||||
}
|
||||
|
||||
autoloadModules = [];
|
||||
return instance.emit('start', get.bind(undefined, undefined));
|
||||
}
|
||||
catch (exception) {
|
||||
log ('error', exception);
|
||||
}
|
||||
};
|
||||
|
||||
//Stop the application
|
||||
this.stop = function () {
|
||||
return instance.emit('stop', get.bind(undefined, undefined));
|
||||
};
|
||||
|
||||
//Restart the application
|
||||
this.restart = function () {
|
||||
if (instance.hasListener('restart')) {
|
||||
return instance.emit('restart', get.bind(undefined, undefined));
|
||||
}
|
||||
else if (instance.hasListener('stop')){
|
||||
|
||||
var promise = instance.stop();
|
||||
|
||||
if (promise && promise.then) {
|
||||
promise.then(function() {
|
||||
instance.start();
|
||||
});
|
||||
}
|
||||
else {
|
||||
instance.start();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Log
|
||||
function log (level, message) {
|
||||
if (instance.hasListener('log')) {
|
||||
instance.emit('log', get.bind(undefined, undefined), message);
|
||||
}
|
||||
else {
|
||||
console.log('['+level+']', message);
|
||||
|
||||
if (level === 'error') {
|
||||
if (message instanceof Error) {
|
||||
throw message;
|
||||
}
|
||||
else if (message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Function passed to every modular module
|
||||
function get (caller, name) {
|
||||
var mod;
|
||||
|
||||
//Search for the module in caller namespace first
|
||||
if (caller && modules.has (caller.namespace && caller.namespace.length > 0
|
||||
? caller.namespace + '.' + name
|
||||
: name)) {
|
||||
mod = modules.get (
|
||||
caller.namespace && caller.namespace.length > 0
|
||||
? caller.namespace + '.' + name
|
||||
: name);
|
||||
}
|
||||
else if (modules.has(name)) {
|
||||
mod = modules.get(name);
|
||||
}
|
||||
|
||||
//If the module as been found
|
||||
if (mod) {
|
||||
if (caller) {
|
||||
if (mod.childrenOf === undefined) {
|
||||
mod.childrenOf = [];
|
||||
}
|
||||
|
||||
if (mod.childrenOf.indexOf(caller) < 0) {
|
||||
mod.childrenOf.push(caller);
|
||||
}
|
||||
|
||||
if (caller.parentOf === undefined) {
|
||||
caller.parentOf = [];
|
||||
}
|
||||
|
||||
if (caller.parentOf.indexOf(mod) < 0) {
|
||||
caller.parentOf.push(mod);
|
||||
}
|
||||
}
|
||||
|
||||
return loadModule(mod);
|
||||
}
|
||||
else {
|
||||
log ('error', 'Unresolved dependency ' + name);
|
||||
}
|
||||
}
|
||||
|
||||
function loadModule (mod) {
|
||||
if (mod.module === undefined) {
|
||||
mod.module = require(mod.fullPath);
|
||||
}
|
||||
|
||||
if (mod.module instanceof Function) {
|
||||
let instanceModule = mod.module(get.bind(undefined,mod));
|
||||
return instanceModule ? instanceModule : mod.module;
|
||||
}
|
||||
else if (mod.module.onLoad instanceof Function){
|
||||
let instanceModule = mod.module.onLoad(get.bind(undefined, mod));
|
||||
return instanceModule ? instanceModule : mod.module;
|
||||
}
|
||||
else {
|
||||
return mod.module;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Register a new module
|
||||
//file : path of the module
|
||||
//name (optional) : override the full name of the module (namespace and name)
|
||||
//override (optional) : if a module with the same full name is already registered, it's overriden
|
||||
function register (options) {
|
||||
var autoload = false;
|
||||
var opts = options;
|
||||
|
||||
//Default options
|
||||
if (opts.override === undefined) {
|
||||
opts.override = false;
|
||||
}
|
||||
|
||||
if (opts.liveReload === undefined) {
|
||||
opts.liveReload = settings.liveReload;
|
||||
}
|
||||
|
||||
opts.fullPath = path.isAbsolute(opts.path) ? opts.path : path.resolve(opts.path);
|
||||
|
||||
//If the file is excluded
|
||||
if (settings.exclude.indexOf(opts.fullPath) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.fullPath.endsWith('autoload.js')) {
|
||||
autoload=true;
|
||||
}
|
||||
|
||||
//Name parameter override module namespace and module name
|
||||
if (opts.namespace === undefined && opts.name === undefined) {
|
||||
//Every folder of the module path is a part of the namespace
|
||||
opts.namespace = path.relative((opts.basePath || settings.basePath) , path.dirname(opts.fullPath))
|
||||
.replace(/\./g,'')
|
||||
.replace(/\//g,'.')
|
||||
.replace(/^\.*/g,'');
|
||||
|
||||
//Name is the basename of the module
|
||||
opts.name = path.basename(opts.fullPath, autoload ? '.autoload.js' : path.extname(opts.fullPath));
|
||||
|
||||
//Replace all . char in class name by _
|
||||
if (opts.name.indexOf('.') >= 0) {
|
||||
opts.name = opts.name.replace(/\./g,'_');
|
||||
}
|
||||
|
||||
//Adding the base namespace
|
||||
if (opts.baseNamespace !== undefined) {
|
||||
opts.namespace = opts.baseNamespace.replace('/\./g','_') + '.' + opts.namespace;
|
||||
}
|
||||
}
|
||||
|
||||
//Calling the module loading hook
|
||||
if (instance.hasListener('loadModule') && instance.emit('loadModule', opts) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var fullName = opts.namespace && opts.namespace.length > 0 ? opts.namespace + '.' + opts.name : opts.name;
|
||||
|
||||
var alreadyLoaded = modules.has(fullName);
|
||||
|
||||
if (!alreadyLoaded || opts.override) {
|
||||
if (alreadyLoaded && opts.fullPath !== modules.get(fullName).fullPath) {
|
||||
//Delete overriden module cache
|
||||
delete require.cache[modules.get(fullName).fullPath];
|
||||
}
|
||||
|
||||
if (settings.logLevel > 0) {
|
||||
log ('info', (fullName + ' loaded from ' + opts.fullPath));
|
||||
}
|
||||
|
||||
opts.module = undefined;
|
||||
|
||||
//Clear the cache of the module if exists
|
||||
if (require.cache[opts.fullPath]) {
|
||||
delete require.cache[opts.fullPath];
|
||||
}
|
||||
|
||||
//Autoload the module
|
||||
if (autoload) {
|
||||
autoloadModules.push(opts);
|
||||
}
|
||||
|
||||
modules.set(fullName, opts);
|
||||
|
||||
if (settings.liveReload || opts.liveReload) {
|
||||
watchedFiles.set(opts.fullPath, opts);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (settings.logLevel > 0) {
|
||||
log ('info', fullName + ' skipped from ' + opts.fullPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//Load all the modules and submodules from a specified path
|
||||
function load (options) {
|
||||
if (!Array.isArray(options.path)) {
|
||||
options.path = [options.path];
|
||||
}
|
||||
|
||||
for (var file of options.path) {
|
||||
var opts = clone(options);
|
||||
var fullPath;
|
||||
|
||||
opts.path = file;
|
||||
|
||||
if (!path.isAbsolute(file)) {
|
||||
fullPath = path.resolve(file);
|
||||
}
|
||||
else {
|
||||
fullPath = file;
|
||||
}
|
||||
|
||||
var stats = fs.statSync(fullPath);
|
||||
|
||||
if (stats.isFile()) {
|
||||
if (settings.liveReload || options.liveReload) {
|
||||
watch (fullPath);
|
||||
}
|
||||
|
||||
register(opts);
|
||||
}
|
||||
else if (stats.isDirectory()) {
|
||||
loadFolder(fullPath, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Load files from a path.
|
||||
//Since it's only used before the application start, file operations are sync
|
||||
function loadFolder (filePath, options) {
|
||||
if (settings.liveReload || options.liveReload) {
|
||||
watch(filePath);
|
||||
}
|
||||
|
||||
var files = fs.readdirSync(filePath);
|
||||
for (var file of files) {
|
||||
var fullPath = path.join(filePath, file);
|
||||
var stats = fs.statSync(fullPath);
|
||||
|
||||
if (stats.isFile && path.extname(fullPath) === '.js') {
|
||||
let opts = clone(options);
|
||||
opts.path = fullPath;
|
||||
|
||||
register(opts);
|
||||
}
|
||||
else if (stats.isDirectory()) {
|
||||
loadFolder(fullPath, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Watch a folder for changes
|
||||
function watch(watchedPath) {
|
||||
var reloadTimeout;
|
||||
var modifiedFiles = [];
|
||||
|
||||
//Search for module already watched in this directory
|
||||
var itWatch = watchers.keys();
|
||||
var cur = itWatch.next();
|
||||
|
||||
var fileStats = fs.statSync(watchedPath);
|
||||
var isFile = true;
|
||||
|
||||
if (watchers.has(watchedPath)) {
|
||||
//Path is already watched
|
||||
return;
|
||||
}
|
||||
else if (fileStats.isDirectory()) {
|
||||
//Close all watchers aiming files in the directory
|
||||
while (cur && cur.value) {
|
||||
if (path.extname(cur.value) === '.js'
|
||||
&& path.dirname(cur.value) === watchedPath) {
|
||||
var watchedFile = watchers.get(cur.value);
|
||||
watchedFile.close();
|
||||
watchers.delete(cur.value);
|
||||
}
|
||||
cur = itWatch.next();
|
||||
}
|
||||
isFile = false;
|
||||
}
|
||||
else if (fileStats.isFile()){
|
||||
//Target is a file in a directory already watched
|
||||
var directory = path.dirname(watchedPath);
|
||||
if (watchers.has(directory)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Watcher is used to detect file changes
|
||||
var watcher = fs.watch(watchedPath, function (ev, fileName) {
|
||||
var fullName = isFile ? watchedPath : path.join(watchedPath, fileName);
|
||||
|
||||
//Some editors like vim will actually rename the file, then modify it.
|
||||
//If temporary file is used, the last event can be on a file that does not exists any more
|
||||
//We listen for all the events until 1 second occurs without anything. Then we treat all modified files
|
||||
if (modifiedFiles.indexOf(fullName) < 0) {
|
||||
modifiedFiles.push(fullName);
|
||||
}
|
||||
|
||||
if (reloadTimeout) {
|
||||
clearTimeout(reloadTimeout);
|
||||
}
|
||||
|
||||
reloadTimeout = setTimeout(function() {
|
||||
reloadTimeout = undefined;
|
||||
var stats;
|
||||
|
||||
for (let file of modifiedFiles) {
|
||||
let exists = true;
|
||||
|
||||
//Check if the file exists
|
||||
try {
|
||||
stats = fs.statSync(file);
|
||||
}
|
||||
catch (err) {
|
||||
exists = false;
|
||||
}
|
||||
|
||||
var isModule = path.extname(file) === '.js';
|
||||
var module = isModule ? watchedFiles.get(file) : null;
|
||||
var fileWatcher = watchers.has(file) ? watchers.get(file) : null;
|
||||
var needRestart = false;
|
||||
var clearParentsCache = false;
|
||||
|
||||
if (exists) {
|
||||
if (stats.isFile() && isModule) {
|
||||
//This is a module
|
||||
try {
|
||||
if (module !== null) {
|
||||
if (!module.override) {
|
||||
module.override = true;
|
||||
}
|
||||
clearParentsCache = true;
|
||||
}
|
||||
register(module);
|
||||
needRestart = settings.restartOnChange;
|
||||
}
|
||||
catch (exception) {
|
||||
if (settings.logLevel >= 0) {
|
||||
log ('error','Module '+file+ ' not reloaded : ' + exception );
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (stats.isDirectory() && !watchers.has(file)) {
|
||||
watch(file);
|
||||
}
|
||||
|
||||
//Reset the watcher if needed
|
||||
if (ev === 'rename' && fileWatcher !== null) {
|
||||
fileWatcher.close();
|
||||
watchers.delete(file);
|
||||
watch(file);
|
||||
}
|
||||
}
|
||||
else if (module !== null) {
|
||||
unRegister(module);
|
||||
|
||||
if (settings.logLevel > 0) {
|
||||
log ('info', module.fullName + ' unregistered');
|
||||
}
|
||||
|
||||
clearParentsCache = true;
|
||||
needRestart = settings.restartOnChange;
|
||||
}
|
||||
else if (fileWatcher !== null) {
|
||||
fileWatcher.close();
|
||||
watchers.delete(file);
|
||||
}
|
||||
|
||||
if (clearParentsCache) {
|
||||
if (module.childrenOf !== undefined) {
|
||||
for (let parentMod of module.childrenOf) {
|
||||
clearCache(parentMod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needRestart) {
|
||||
instance.restart();
|
||||
}
|
||||
}
|
||||
|
||||
modifiedFiles = [];
|
||||
|
||||
},1000);
|
||||
});
|
||||
|
||||
watchers.set(watchedPath, watcher);
|
||||
}
|
||||
|
||||
//Remove a module and clear its nodejs cache
|
||||
function unRegister (module) {
|
||||
if (modules.has(module.fullName)){
|
||||
if (module.parentOf !== undefined) {
|
||||
for (var children of module.parentOf) {
|
||||
if (children.childrenOf !== undefined && children.childrenOf.length > 0) {
|
||||
children.childrenOf.splice(children.childrenOf.indexOf(module), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules.delete(module.fullName);
|
||||
delete require.cache[module.file];
|
||||
}
|
||||
}
|
||||
|
||||
//Clear module and all depedencies cache
|
||||
function clearCache(module) {
|
||||
if (module.childrenOf !== undefined) {
|
||||
for (let parentMod of module.chidrenOf) {
|
||||
clearCache(parentMod);
|
||||
}
|
||||
}
|
||||
module.override = true;
|
||||
|
||||
register(module);
|
||||
}
|
||||
}
|
||||
|
||||
function clone(obj) {
|
||||
var object = {};
|
||||
|
||||
for (let prop in obj) {
|
||||
if (Array.isArray(obj[prop])) {
|
||||
object[prop] = obj[prop].slice(0);
|
||||
}
|
||||
else {
|
||||
object[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
Modular.prototype = new SyncEventEmitter();
|
||||
Modular.prototype.constructor = SyncEventEmitter;
|
||||
|
||||
module.exports = function (settings) {
|
||||
return new Modular(settings);
|
||||
};
|
||||
Reference in New Issue
Block a user