'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); } else { return require(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); };