552 lines
13 KiB
JavaScript
552 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 has 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.instance === undefined) {
|
|
mod.module = require(mod.fullPath);
|
|
|
|
if (mod.module instanceof Function) {
|
|
mod.instance = mod.module(get.bind(undefined,mod));
|
|
}
|
|
else if (mod.module.onLoad instanceof Function){
|
|
mod.module.onLoad(get.bind(undefined, mod));
|
|
mod.instance = mod.module;
|
|
}
|
|
else {
|
|
mod.instance = mod.module;
|
|
}
|
|
}
|
|
|
|
return mod.instance;
|
|
}
|
|
|
|
|
|
//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;
|
|
opts.instance = 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);
|
|
};
|