Files
modular/lib/modular.js

550 lines
13 KiB
JavaScript

'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 () {
//TODO Stop watchers if there's any
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) {
//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,'');
}
if (opts.name === undefined) {
//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);
let opts = clone(options);
if (stats.isFile && path.extname(fullPath) === '.js') {
opts.path = fullPath;
register(opts);
}
else if (stats.isDirectory()) {
if (opts.namespace !== undefined) {
opts.namespace = opts.namespace + '.' + file;
}
if (opts.name) {
opts.name = undefined;
}
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);
};