we-core
Version:
We.js is a node.js framework for build real time applications, sites or blogs!
539 lines (471 loc) • 15.2 kB
JavaScript
/**
* We.js main file, load we.js core and features
*/
// Module dependencies.
// Disable uneed bluebird warnings:
process.env.BLUEBIRD_W_FORGOTTEN_RETURN = '0';
const http = require('http'),
_ = require('lodash'),
path = require('path'),
staticConfig = require('./staticConfig'),
Database = require('./Database'),
Hooks = require('./Hooks'),
PluginManager = require('./PluginManager'),
Router = require('./Router'),
Sanitizer = require('./Sanitizer'),
EventEmiter = require('events'),
unhandledErrorCatcher = require('./unhandledErrorCatcher'),
{ readFileSync, writeFileSync, writeFile } = require('fs');
/**
* We.js object
*
* @type {Object}
*/
function We (options) {
if (!options) options = {};
const we = this;
this.packageJSON = require('../package.json');
this.config = options;
this.childProcesses = [];
this.plugins = {};
this.pluginPaths = [];
this.pluginNames = [];
// controllers
this.controllers = {};
this.projectPath = options.projectPath || process.cwd();
this.projectPackageJSON = require(path.resolve(this.projectPath, 'package.json'));
this.projectConfigFolder = options.projectConfigFolder || path.join(this.projectPath, 'config');
// start configs with static configs
this.config = staticConfig(this.projectPath, this);
// enviroment config prod | dev | test
we.env = options.env || require('./getEnv.js')();
// winston logger
we.log = require('./log')(we);
// hooks and events
we.hooks = new Hooks();
we.events = new EventEmiter();
// we.js events is controlled then we will use an max of 70 listenners in every event:
we.events.setMaxListeners(70);
// we.js sanitizer
we.sanitizer = new Sanitizer(this);
// The we.js router
we.router = new Router(this);
// we.js prototypes
we.class = {
Controller: require('./class/Controller.js'),
Theme: require('./class/Theme.js')(we),
Plugin: require('./class/Plugin.js')(we)
};
// database logic and models is avaible in we.db.models
we.db = new Database(this);
// - init database
we.db.defaultConnection = we.db.connect(we.env);
//set for more compatbility with sequelize docs
we.db.sequelize = we.db.defaultConnection;
// plugin manager and plugins vars
we.pluginManager = new PluginManager(this);
// Error catcher to get unhandled erros, use this options to disable on multiple we.js instances
if (!we.config.disableGlobalErrorCatcher) {
unhandledErrorCatcher(we);
}
switch (we.config.bootstrapMode) {
case 'install':
case 'installation':
case 'update':
we.hooks.on('bootstrap', [
we.bootstrapFunctions.checkDBConnection,
we.bootstrapFunctions.loadCoreFeatures,
we.bootstrapFunctions.loadPluginFeatures,
we.bootstrapFunctions.initI18n,
we.bootstrapFunctions.loadTemplateCache,
we.bootstrapFunctions.instantiateModels,
we.bootstrapFunctions.syncModels,
we.bootstrapFunctions.loadControllers,
we.bootstrapFunctions.installAndRegisterPlugins
]);
break;
case 'complete':
case 'full':
case 'test':
// full load, usefull for tests
we.hooks.on('bootstrap', [
we.bootstrapFunctions.checkDBConnection,
we.bootstrapFunctions.loadCoreFeatures,
we.bootstrapFunctions.loadPluginFeatures,
we.bootstrapFunctions.initI18n,
we.bootstrapFunctions.loadTemplateCache,
we.bootstrapFunctions.instantiateModels,
we.bootstrapFunctions.syncModels,
we.bootstrapFunctions.loadControllers,
we.bootstrapFunctions.installAndRegisterPlugins,
we.bootstrapFunctions.setExpressApp,
we.bootstrapFunctions.passport,
we.bootstrapFunctions.createDefaultFolders,
we.bootstrapFunctions.registerAllViewTemplates,
we.bootstrapFunctions.mergeRoutes,
we.bootstrapFunctions.bindResources,
we.bootstrapFunctions.bindRoutes
]);
break;
default:
// defaults to load for run
we.hooks.on('bootstrap', [
we.bootstrapFunctions.checkDBConnection,
we.bootstrapFunctions.loadCoreFeatures,
we.bootstrapFunctions.loadPluginFeatures,
we.bootstrapFunctions.initI18n,
we.bootstrapFunctions.loadTemplateCache,
we.bootstrapFunctions.instantiateModels,
we.bootstrapFunctions.syncModels,
we.bootstrapFunctions.loadControllers,
we.bootstrapFunctions.installAndRegisterPlugins,
we.bootstrapFunctions.setExpressApp,
we.bootstrapFunctions.passport,
we.bootstrapFunctions.createDefaultFolders,
we.bootstrapFunctions.registerAllViewTemplates,
we.bootstrapFunctions.mergeRoutes,
we.bootstrapFunctions.bindResources,
we.bootstrapFunctions.bindRoutes
]);
}
}
We.prototype = {
/**
* Set config in config/configuration.json file
*
* @param {String} variable path to the variable
* @param {String} value
* @param {Function} cb callback
*/
setConfig(variable, value, cb) {
if (!cb) cb = function(){};
let cJSON,
cFGpath = path.join(this.projectPath, '/config/configuration.json');
try {
cJSON = JSON.parse(readFileSync(cFGpath));
} catch(e) {
if (e.code == 'ENOENT') {
writeFileSync(cFGpath, '{}');
cJSON = {};
} else {
return cb(e);
}
}
if (value == 'true') value = true;
if (value == 'false') value = false;
_.set(cJSON, variable, value);
writeFile(cFGpath, JSON.stringify(cJSON, null, 2), cb);
},
/**
* Unset config in config/configuration.json file
*
* @param {String} variable path to the variable
* @param {String} value
* @param {Function} cb callback
*/
unSetConfig(variable, cb) {
let cJSON,
cFGpath = path.join(this.projectPath, '/config/configuration.json');
try {
cJSON = JSON.parse(readFileSync(cFGpath));
} catch(e) {
if (e.code == 'ENOENT') {
writeFileSync(cFGpath, '{}');
cJSON = {};
} else {
return cb(e);
}
}
_.unset(cJSON, variable);
writeFile(cFGpath, JSON.stringify(cJSON, null, 2), cb);
},
// set bootstrap functions
bootstrapFunctions: require('./bootstrapFunctions'),
// flag to check if this we.js instance did the bootstrap
bootstrapStarted: false,
// flag to check if needs restart
needsRestart: false,
// we.utils.async, we.utils._ ... see the ./utils file
utils: require('./utils'),
// load we.js responses
responses: require('./responses'),
// save we-core path to plugin.js for update e install process
weCorePluginfile: path.resolve(__dirname, '../') + '/plugin.js',
//Overide default toString and inspect to custom infos in we.js object
inspect() {
return '\nWe.js ;)\n';
},
toString() {
return this.inspect();
},
// client side config generator
getAppBootstrapConfig: require('./staticConfig/getAppBootstrapConfig.js'),
/**
* Bootstrap and initialize the app
*
* @param {Object} configOnRun optional
* @param {Function} cb callback to run after load we.js
*/
bootstrap(configOnRun, cb) {
const we = this;
// only bootstrap we.js one time
if (we.bootstrapStarted) throw new Error('We.js already did bootstrap');
we.bootstrapStarted = true;
// configsOnRun object is optional
if (!cb) {
cb = configOnRun;
configOnRun = null;
}
// configs on run extends default and file configs
if (configOnRun) _.merge(we.config, configOnRun);
// run the bootstrap hook
we.hooks.trigger('bootstrap', we, function bootstrapDone (err) {
if (err) {
console.log('Error on bootstrap: ');
throw err;
}
we.events.emit('we:bootstrap:done', we);
we.log.debug('We.js bootstrap done');
// set pm2 gracefullReload:
we.setPM2grShutdownFN(we);
return cb(null, we);
});
},
/**
* Start we.js server (express)
* use after we.bootstrap
*
* @param {Function} cb callback how returns with cb(err);
*/
startServer(cb) {
if (!cb) cb = function(){}; // cb is optional
let we = this;
we.hooks.trigger('we:server:before:start', we, function afterRunBeforeServerStart (err) {
if (err) return cb(err);
/**
* Get port from environment and store in Express.
*/
let port = normalizePort(we.config.port);
we.express.set('port', port);
/**
* Create HTTP server with suport to url alias rewrite
*/
const server = http.createServer(function onCreateServer (req, res){
Object.defineProperty(req, 'we', {
get: function getWe() { return we; }
});
// Clean memory on end events:
res.on('finish', function() {
we.freeResponseMemory(req, res);
});
res.on('close', function() {
we.freeResponseMemory(req, res);
});
res.on('end', function() {
we.freeResponseMemory(req, res);
});
// suport for we.js widget API
// install we-plugin-widget to enable this feature
if (req.headers && req.headers['we-widget-action'] && req.method == 'POST') {
req.isWidgetAction = true;
req.originalMethod = req.method;
req.method = 'GET'; // widgets only are associated to get routes
}
// parse extension if not is public folder
if (!we.router.isPublicFolder(req.url)) {
req.extension = we.router.splitExtensionFromURL(req.url);
if (req.extension) {
let format = we.utils.mime.getType(req.extension);
if (req.we.config.responseTypes.includes(format)) {
// update the header response format with extension format
req.headers.accept = format;
// update the old url
req.url = req.url.replace('.'+req.extension, '');
}
}
}
// install we-plugin-url-alias to enable alias feature
if (we.plugins['we-plugin-url-alias'] && we.config.enableUrlAlias) {
we.router.alias.httpHandler.bind(this)(req, res);
} else {
we.express.bind(this)(req, res);
}
});
we.events.emit('we:server:after:create', { we: we, server: server });
/**
* Listen on provided port, on all network interfaces.
*/
// catch 404 and forward to error handler
we.express.use(function routeNotFound (req, res) {
we.log.debug('Route not found:', {
method: req.method,
path: req.path
});
// var err = new Error('Not Found');
// err.status = 404;
return res.notFound();
});
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort (val) {
let port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError (error) {
if (error.syscall !== 'listen') {
throw error;
}
let bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening () {
let addr = server.address(),
bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
we.log.info('Run in '+we.env+' enviroment and listening on ' + bind);
if (process.send) {
process.send('ready');
}
}
// save the current http server
we.http = server;
// express error handlers
// will print stacktrace
we.express.use(function onExpressError (err, req, res, next) {
// invalid url error handling
if (err instanceof URIError) {
we.log.warn('onExpressError:URIError on:', {
path: req.path,
error: err
});
res.addMessage('warning', {
text: 'router.invalid.url'
});
// go to home page on invalid url request
return res.goTo('/');
}
we.log.error('onExpressError:Error on:', {
path: req.path,
error: err
});
res.serverError(err);
});
we.hooks.trigger('we:server:after:start',we, function afterHookAfterStart (err) {
cb(err, we);
});
});
},
/**
* Bootstrap and Start we.js server
*
* @param {Object} cfgs configs (optional)
* @param {Function} cb callback
*/
go(cfgs, cb) {
if (!cb) {
cb = cfgs;
cfgs = {};
}
this.bootstrap(cfgs, function afterBootstrapOnWeGo (err, we) {
if (err) throw err;
we.startServer(cb);
});
},
/**
* Turn off process function
*
* @param {Function} cb callback
*/
exit(cb) {
this.isExiting = true;
// close db connection
if (this.db.defaultConnection) {
this.db.defaultConnection.close();
}
// close all loggers and wait for end writes
this.log.closeAllLoggersAndDisconnect(this, cb);
},
/**
* Helper function to delete objects from response for help GC
*/
freeResponseMemory(req, res) {
if (res.locals) {
res.locals.regions = {};
res.locals.currentUser = {};
}
delete req.user;
},
/**
* Run all plugin and project cron tasks
*
* @param {Function} cb callback
*/
runCron(cb) {
this.cron = require('./cron');
this.cron.loadAndRunAllTasks(this, cb);
},
/**
* set pm2 gracefullReload
*/
setPM2grShutdownFN(we) {
// old/windows version:
process.on('message', (msg)=> {
if (msg === 'shutdown') {
// disconect, and exit
we.exit( (err)=> { process.exit(err ? 1 : 0); });
}
});
// new version:
process.on('SIGINT', ()=> {
we.exit( (err)=> { process.exit(err ? 1 : 0); });
});
}
};
// Add default toJSON method in errors to get better erros:
if (!('toJSON' in Error.prototype)) {
Object.defineProperty(Error.prototype, 'toJSON', {
value: function () {
const alt = {};
Object.getOwnPropertyNames(this).forEach(function (key) {
alt[key] = this[key];
}, this);
return alt;
},
configurable: true,
writable: true
});
}
module.exports = We;