Initial commit
This commit is contained in:
152
README.md
Normal file
152
README.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# MODULAR
|
||||||
|
Path abstraction and mocks for NodeJS.
|
||||||
|
|
||||||
|
## Functionalities
|
||||||
|
* Automatically load javascript modules
|
||||||
|
* Abstract modules path. Access any module using namespace and module name
|
||||||
|
* Easy mock
|
||||||
|
* Live reload of changes
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
```javascript
|
||||||
|
var Modular = require('modular.js');
|
||||||
|
|
||||||
|
var application = new Modular({
|
||||||
|
files : './app',
|
||||||
|
liveReload : true,
|
||||||
|
restartOnChanges : true,
|
||||||
|
}).on ('start', function(modular) {
|
||||||
|
modular('main').start();
|
||||||
|
}).on ('stop', function(modular) {
|
||||||
|
modular('main').stop();
|
||||||
|
}).on ('log', function (modular, level, message, error) {
|
||||||
|
console.log(error);
|
||||||
|
}).start();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
The modular constructor accept several settings :
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
/* files can be a string for one file or folder */
|
||||||
|
files : 'app/module.js',
|
||||||
|
/* or an array to load several files or folders */
|
||||||
|
files : [ 'app/module.js', 'app/module2.js', 'app/subfolder' ],
|
||||||
|
/* an object allowing to setup the way the modules are loaded */
|
||||||
|
files : {
|
||||||
|
/* the path is either a string for one file or folder */
|
||||||
|
path : 'app/module.js',
|
||||||
|
/* or an array */
|
||||||
|
path : [ 'app/module.js', 'app/module2.js', 'app/subfolder' ]
|
||||||
|
override : true, /* modules override already loaded modules in case of collision
|
||||||
|
default value : false */
|
||||||
|
namespace : 'namespace', /* namespace is appended before the deduced namespace */
|
||||||
|
basePath : 'path/path2', /* the base path used to deduce the namespace of modules
|
||||||
|
default value : path value (when path is a directory) */
|
||||||
|
fullName : 'namespace.name',/* specify the full name of the module
|
||||||
|
Only when path is one file. Doesn't work with namespace setting */
|
||||||
|
liveReload : true, /* enable or disable liveReload on a file/folder basis
|
||||||
|
default value : false */
|
||||||
|
},
|
||||||
|
basePath : /* Path used to deduce the namespace
|
||||||
|
default value : path of the file that load Modular */
|
||||||
|
liveReload : true, /* enable live modification reload for whole application
|
||||||
|
default value : false */
|
||||||
|
restartOnChange : true, /* automaticaly restart application when a module is reloaded
|
||||||
|
default value : liveReload value */
|
||||||
|
exclude : ['app/module3.js'], /* those files won't be loaded */
|
||||||
|
default value : file that load Modular
|
||||||
|
logLevel : 0 /* level of logging. 0 : error, 1 : info, 2 : verbose
|
||||||
|
default value : 0 */
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
Modular communicate through messages. Those event aren't asynchronous.
|
||||||
|
This way, modular won't try to start application before it stopped.
|
||||||
|
If start or stop event return a Promise object, modular will wait for the Promise to be fulfilled to continue.
|
||||||
|
|
||||||
|
### start
|
||||||
|
Emitted when the application must be started.
|
||||||
|
* Parameter : modular
|
||||||
|
* Return : Promise (optional)
|
||||||
|
|
||||||
|
### stop
|
||||||
|
Emitted when the application must be stopped.
|
||||||
|
This event is optional
|
||||||
|
* Parameter : modular
|
||||||
|
* Return : Promise (optional)
|
||||||
|
|
||||||
|
### restart
|
||||||
|
Emitted when the application must be restarted.
|
||||||
|
This event is optional
|
||||||
|
If this event is not used, modular will emit the stop event followed by start
|
||||||
|
* Parameter : modular
|
||||||
|
* Return : Promise (optional)
|
||||||
|
|
||||||
|
### log
|
||||||
|
Emitted whenever log occurs.
|
||||||
|
This event is optional.
|
||||||
|
If this event is not defined, logs are written on stdout.
|
||||||
|
* Parameter : modular, message, error
|
||||||
|
* Return : nothing
|
||||||
|
|
||||||
|
### loadModule
|
||||||
|
Emitted whenever a module will be loaded.
|
||||||
|
This event is optional.
|
||||||
|
You can change the settings used to load the module.
|
||||||
|
* Parameter : modular, settings (module parameters)
|
||||||
|
* Return : false to cancel module loading
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
Modular modules must look likes
|
||||||
|
```javascript
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
var mod = modular('namespace.module');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Or
|
||||||
|
```javascript
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports.onLoad = function (modular) {
|
||||||
|
var mod = modular('namespace.module');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Modular will simply call those functions each time the module is required by another.
|
||||||
|
Those function are never invoked using the new statement and are called even if the nodejs module is already cached.
|
||||||
|
This means that each module manage its instance (a module can act as a singleton for example, another can always return a new instance).
|
||||||
|
|
||||||
|
If the module does not directly exports a function or does not export onLoad function, the module is return as it is, but won't be able to load any modular module.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
### Exclude your main javascript file
|
||||||
|
Loading the main javascript file with Modular is useless, watching it for changes can lead to weird behavior.
|
||||||
|
If no file is excluded, Modular will exclude its parent by default.
|
||||||
|
|
||||||
|
You can still load the directory containing your main file by excluding it. The easiest way is to use the node __filename property :
|
||||||
|
```javascript
|
||||||
|
var Modular = require('modular.js');
|
||||||
|
|
||||||
|
var application = new Modular({
|
||||||
|
files : './',
|
||||||
|
exclude : [ __filename],
|
||||||
|
liveReload : true,
|
||||||
|
restartOnChange : true,
|
||||||
|
}).on ('start', function(modular) {
|
||||||
|
modular('main').start();
|
||||||
|
}).on ('stop', function(modular) {
|
||||||
|
modular('main').stop();
|
||||||
|
}).on ('log', function (modular, level, message, err) {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
```
|
||||||
|
|
||||||
|
###
|
||||||
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);
|
||||||
|
};
|
||||||
39
lib/synceventemitter.js
Normal file
39
lib/synceventemitter.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function SyncEventEmitter () {
|
||||||
|
this.listeners = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncEventEmitter.prototype.on = function (ev, callback) {
|
||||||
|
if (this.listeners.has(ev)) {
|
||||||
|
this.listeners.remove(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.set(ev, callback);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
SyncEventEmitter.prototype.emit = function (ev) {
|
||||||
|
if (this.listeners.has(ev)) {
|
||||||
|
let params = [];
|
||||||
|
|
||||||
|
for (var i = 1 ; i < arguments.length ; ++i) {
|
||||||
|
params.push(arguments[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.listeners.get(ev).apply(undefined, params);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SyncEventEmitter.prototype.removeListener = function (ev) {
|
||||||
|
if (this.listeners.has(ev)) {
|
||||||
|
this.listeners.remove(ev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SyncEventEmitter.prototype.hasListener = function (ev) {
|
||||||
|
return this.listeners.has(ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = SyncEventEmitter;
|
||||||
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "modular",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Module path abstraction and mock framework",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://lab.cherboiche.org/boudin/modular.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"module",
|
||||||
|
"mock",
|
||||||
|
"live",
|
||||||
|
"reload"
|
||||||
|
],
|
||||||
|
"author": "boudin",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"mocha": "^2.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
test/autoload/test.autoload.js
Normal file
4
test/autoload/test.autoload.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
'use strict';
|
||||||
|
module.exports = function() {
|
||||||
|
throw new Error('autoloaded');
|
||||||
|
};
|
||||||
9
test/constructor/nested/four.js
Normal file
9
test/constructor/nested/four.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/constructor/nested/last/five.js
Normal file
9
test/constructor/nested/last/five.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/constructor/one.js
Normal file
9
test/constructor/one.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/constructor/two.js
Normal file
9
test/constructor/two.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/loading/module.js
Normal file
9
test/loading/module.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
return {
|
||||||
|
run: function () {
|
||||||
|
return modular('module2').run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/loading/module2.js
Normal file
9
test/loading/module2.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/loading/module3.js
Normal file
9
test/loading/module3.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
return {
|
||||||
|
run: function () {
|
||||||
|
return modular('nested.module4').run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
11
test/loading/module5.js
Normal file
11
test/loading/module5.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var mod;
|
||||||
|
|
||||||
|
module.exports.onLoad = function (modular) {
|
||||||
|
mod = modular('module6');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.run = function () {
|
||||||
|
return mod.run();
|
||||||
|
};
|
||||||
9
test/loading/module6.js
Normal file
9
test/loading/module6.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/loading/nested/module4.js
Normal file
9
test/loading/nested/module4.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/override/constructor/one.js
Normal file
9
test/override/constructor/one.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
9
test/override/constructor/two.js
Normal file
9
test/override/constructor/two.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (modular) {
|
||||||
|
return {
|
||||||
|
run : function () {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
244
test/test.js
Normal file
244
test/test.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* Unit tests for modular
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var modular = require('../lib/modular.js');
|
||||||
|
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
//Load a file without any options
|
||||||
|
describe('Modular', function() {
|
||||||
|
describe('Settings', function() {
|
||||||
|
it('load a single module', function() {
|
||||||
|
modular({
|
||||||
|
files : './constructor/one.js',
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('constructor.one').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load a single module with settings', function() {
|
||||||
|
modular({
|
||||||
|
files : {
|
||||||
|
path: './constructor/one.js',
|
||||||
|
}
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('constructor.one').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load two modules', function() {
|
||||||
|
modular({
|
||||||
|
files : [ './constructor/one.js', './constructor/two.js' ],
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('constructor.one').run());
|
||||||
|
assert.equal (2, modular('constructor.two').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load two modules with settings', function() {
|
||||||
|
modular({
|
||||||
|
files : [ {
|
||||||
|
path: './constructor/one.js'
|
||||||
|
}, {
|
||||||
|
path: './constructor/two.js'
|
||||||
|
}],
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('constructor.one').run());
|
||||||
|
assert.equal (2, modular('constructor.two').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load a directory', function() {
|
||||||
|
modular({
|
||||||
|
files : './constructor',
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('constructor.one').run());
|
||||||
|
assert.equal (2, modular('constructor.two').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load a directory with settings', function() {
|
||||||
|
modular({
|
||||||
|
files : {
|
||||||
|
path: './constructor'
|
||||||
|
}
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('constructor.one').run());
|
||||||
|
assert.equal (2, modular('constructor.two').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load a single module with namespace and name', function() {
|
||||||
|
modular({
|
||||||
|
files : {
|
||||||
|
path: './constructor/one.js',
|
||||||
|
namespace: 'namespace',
|
||||||
|
name: 'class'
|
||||||
|
}
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('namespace.class').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load a directories with override', function() {
|
||||||
|
modular({
|
||||||
|
files : ['./constructor', {
|
||||||
|
path: './override',
|
||||||
|
basePath: './override',
|
||||||
|
override: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (3, modular('constructor.one').run());
|
||||||
|
assert.equal (4, modular('constructor.two').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load a directories with no override', function() {
|
||||||
|
modular({
|
||||||
|
files : ['./constructor', {
|
||||||
|
path: './override',
|
||||||
|
override: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('constructor.one').run());
|
||||||
|
assert.equal (2, modular('constructor.two').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load nested directories', function() {
|
||||||
|
modular({
|
||||||
|
files : './constructor'
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (4, modular('constructor.nested.four').run());
|
||||||
|
assert.equal (5, modular('constructor.nested.last.five').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load directory with global base path', function() {
|
||||||
|
modular({
|
||||||
|
files : './constructor',
|
||||||
|
basePath : './constructor'
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('one').run());
|
||||||
|
assert.equal (2, modular('two').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load directory with per directory base path', function() {
|
||||||
|
modular({
|
||||||
|
files : [{
|
||||||
|
path: './constructor',
|
||||||
|
basePath : './constructor'
|
||||||
|
},{
|
||||||
|
path: './override'
|
||||||
|
}]
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('one').run());
|
||||||
|
assert.equal (2, modular('two').run());
|
||||||
|
assert.equal (3, modular('override.constructor.one').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load file with global base path', function() {
|
||||||
|
modular({
|
||||||
|
files : './constructor/one.js',
|
||||||
|
basePath : './constructor'
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('one').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load file with per file base path', function() {
|
||||||
|
modular({
|
||||||
|
files : {
|
||||||
|
path: './constructor/one.js',
|
||||||
|
basePath : './constructor'
|
||||||
|
}
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('one').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load file with base namespace', function() {
|
||||||
|
modular({
|
||||||
|
files : {
|
||||||
|
path: './constructor/one.js',
|
||||||
|
baseNamespace: 'namespace'
|
||||||
|
}
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('namespace.constructor.one').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load directory with base namespace', function() {
|
||||||
|
modular({
|
||||||
|
files : {
|
||||||
|
path: './constructor',
|
||||||
|
baseNamespace: 'namespace'
|
||||||
|
}
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
assert.equal (1, modular('namespace.constructor.one').run());
|
||||||
|
assert.equal (2, modular('namespace.constructor.two').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('autoload modules', function() {
|
||||||
|
assert.throws(function(){
|
||||||
|
modular({
|
||||||
|
files : {
|
||||||
|
path: './autoload',
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
},
|
||||||
|
/autoloaded/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exclude module', function() {
|
||||||
|
assert.throws(function(){
|
||||||
|
modular({
|
||||||
|
files : './constructor',
|
||||||
|
exclude: ['./constructor/two.js']
|
||||||
|
}).on('start', function(modular) {
|
||||||
|
modular('two').run();
|
||||||
|
}).start();
|
||||||
|
},
|
||||||
|
/Unresolved dependency two/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe ('Modules', function() {
|
||||||
|
it('load modules in the same namespace', function() {
|
||||||
|
modular({
|
||||||
|
files: './loading'
|
||||||
|
}).on('start', function (modular) {
|
||||||
|
assert.equal (1, modular('loading.module').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load modules in sub namespace', function() {
|
||||||
|
modular({
|
||||||
|
files: './loading'
|
||||||
|
}).on('start', function (modular) {
|
||||||
|
assert.equal (1, modular('loading.module3').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('access modules through onLoad function', function() {
|
||||||
|
modular({
|
||||||
|
files: ['./loading/module5.js',
|
||||||
|
'./loading/module6.js' ]
|
||||||
|
}).on('start', function (modular) {
|
||||||
|
assert.equal (1, modular('loading.module5').run());
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
/* A tester
|
||||||
|
* Live reload
|
||||||
|
* Autorestart
|
||||||
|
* Log
|
||||||
|
*/
|
||||||
Reference in New Issue
Block a user