/*------------------------------------*\
WEB SERVER
\*------------------------------------*/
/* jslint node: true */
/**
* Inform CLI when the server is up.
* @private
* @function cliServerUp
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
* @param {string} urlOutput URL default access for website.
* @param {string} message Details provide with URL.
*/
function cliServerUp(NA, urlOutput, message) {
var limit = Math.max(message.length + 2, urlOutput.length + 7),
separator = "",
i;
for (i = 0; i < limit; i++) {
separator += "=";
}
NA.log("");
NA.log("\u001B[32m", separator);
NA.log(" " + message);
NA.log("\u001B[32m", " URL: \u001B[31m" + urlOutput);
NA.log("\u001B[32m", separator);
NA.log("");
}
/**
* Calculate the url output for simple web server.
* @private
* @function simpleServerUrlOutput
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
* @param {number} httpPort Port used by application.
* @returns The url Output.
*/
function simpleServerUrlOutput(NA, httpPort) {
var url = NA.modules.url,
hostname = NA.configuration.httpHostname || process.env.IP_ADDRESS || "localhost",
http = (httpPort === 80 && !NA.configuration.httpSecure),
https = (httpPort === 443 && NA.configuration.httpSecure),
port = http || https ? "" : ":" + httpPort,
s = (NA.configuration.httpSecure ? "s" : ""),
path = (typeof NA.configuration.browse === "string") ? NA.configuration.browse : "";
return url.resolve("http" + s + '://' + hostname + port + "/", path);
}
/**
* Listen the port.
* @private
* @function serverListener
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
* @param {number} httpPort Port used by application.
* @param {number} urlOutput The url Output.
* @param {number} messageCode For find the correct JSON message.
* @param {NA~callback} next After server listenning.
*/
function serverListener(NA, httpPort, urlOutput, messageCode, next) {
var path = NA.modules.path;
/* Listen HTTP(s) request. */
NA.server.listen(httpPort, function () {
var data = {
httpPort: httpPort,
path: path.join(NA.serverPath, NA.webconfigName)
},
message = NA.cliLabels.running[messageCode].replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return data[matches]; });
/* Inform CLI the server is running. */
cliServerUp(NA, urlOutput, message);
/* Some action after server listening */
if (next) {
next();
}
/* After website was started. */
if (NA.afterRunning) {
NA.afterRunning.call(NA);
}
});
}
/**
* Catch error from server.
* @private
* @function errorServer
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
*/
function serverError(NA) {
/* Catch error. */
NA.server.on("error", function (error) {
if (error.syscall !== 'listen') {
throw error;
}
/* In case of error. */
switch (error.code) {
case 'EACCES':
NA.log(NA.cliLabels.running.portRequiresPrivileges.replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return error[matches]; }));
/* Kill current process. */
process.exit(1);
break;
case 'EADDRINUSE':
NA.log(NA.cliLabels.running.portAlreadyListened.replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return error[matches]; }));
/* Kill current process. */
process.exit(1);
break;
default:
throw error;
}
});
}
/**
* Set information to avoid cache.
* @private
* @function noCache
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
* @param {boolean} nocache No cache if set to true.
* @param {Object} staticOptions All options for publics file cache.
*/
function noCache(NA, nocache, staticOptions) {
/* No Cache */
if (nocache) {
NA.express.set("etag", false);
NA.express.get("/*", function(request, response, next) {
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", 0);
next();
});
if (staticOptions) {
staticOptions.maxAge = 0;
staticOptions.etag = false;
staticOptions.lastModified = false;
}
}
if (staticOptions) {
return staticOptions;
}
}
/**
* Run project folder with targeted directory (without webconfig) as a « public » directory.
* @private
* @function simpleWebServer
* @memberOf NA#
* @this NA
*/
exports.simpleWebServer = function () {
var NA = this,
fs = NA.modules.fs,
path = NA.modules.path,
http = NA.modules.http,
https = NA.modules.https,
express = NA.modules.express,
compress = NA.modules.compress,
staticOptions = { maxAge: 86400000 * 30 },
opn = NA.modules.opn,
httpPort = NA.configuration.httpPort || process.env.PORT || (NA.configuration.httpSecure ? 443 : 80),
urlOutput = simpleServerUrlOutput(NA, httpPort);
/* Configure the server. */
NA.express = express();
NA.express.set("strict routing", true);
NA.express.set("x-powered-by", false);
NA.express.set("port", httpPort);
staticOptions = noCache(NA, !NA.configuration.cache, staticOptions);
/* Use gzip and others client-server data compression. */
NA.express.use(compress());
/* Provide all files behind possible HTTP Response in the current directory. */
NA.express.use(express.static(NA.serverPath, staticOptions));
/* Create HTTPs server. */
if (NA.configuration.httpSecure && typeof NA.configuration.httpSecure === "string") {
NA.httpsServer = https.createServer({
key: fs.readFileSync(path.join(NA.serverPath, NA.configuration.httpSecure + ".key"), "utf-8"),
cert: fs.readFileSync(path.join(NA.serverPath, NA.configuration.httpSecure + ".crt"), "utf-8")
}, NA.express);
}
NA.httpServer = http.createServer(NA.express);
NA.server = NA.httpsServer || NA.httpServer;
/* Listenning HTTP request. */
serverListener(NA, httpPort, urlOutput, "publicMode", function () {
/* Open url provided at the default page in a tab of default system browser. */
if (NA.configuration.browse) {
opn(urlOutput);
}
});
/* Catche error from Server. */
serverError(NA);
};
/**
* Start a real NodeAtlas Server.
* @public
* @function nodeAtlasWebServer
* @memberOf NA#
* @this NA
* @param {NA~callback} next Passed to the next function.
* @param {NA~fallback} fallback Called if you want generate mode.
*/
exports.nodeAtlasWebServer = function (next, fallback) {
var NA = this,
express = NA.modules.express,
fs = NA.modules.fs,
path = NA.modules.path,
http = NA.modules.http,
https = NA.modules.https;
/**
* Instance of Express.js module.
* @public
* @function express
* @memberOf NA#
*/
NA.express = express();
/* Configure the server. */
NA.express.set("strict routing", true);
NA.express.set("x-powered-by", false);
/* Change Engine */
if (NA.webconfig.engine) {
NA.express.set("view engine", NA.webconfig.engine);
NA.express.set("views", path.join(NA.serverPath, NA.webconfig.viewsRelativePath));
}
noCache(NA, !NA.webconfig.cache);
if (typeof NA.webconfig.httpSecure !== "boolean" && NA.webconfig.httpSecureKeyRelativePath && NA.webconfig.httpSecureCertificateRelativePath) {
/**
* The HTTPs server if exist.
* @public
* @function httpsServer
* @memberOf NA#
* @default undefined
*/
NA.httpsServer = https.createServer({
key: fs.readFileSync(path.join(NA.serverPath, NA.webconfig.httpSecureKeyRelativePath), 'utf-8'),
cert: fs.readFileSync(path.join(NA.serverPath, NA.webconfig.httpSecureCertificateRelativePath), 'utf-8')
}, NA.express);
}
/**
* The HTTP server.
* @public
* @function httpServer
* @memberOf NA#
*/
NA.httpServer = http.createServer(NA.express);
/**
* The Server used to listen.
* @public
* @function server
* @memberOf NA#
*/
NA.server = NA.httpsServer || NA.httpServer;
/* Allow you to parse request and response. */
NA.initMiddlewares();
/* Allow you to parse the Less files. */
NA.initLessProcess();
/* Allow you to parse the Styl files. */
NA.initStylusProcess();
/* Allow you to use Session variables. */
NA.initSessions(next, fallback);
};
/**
* Add middleware for parse request and response.
* @private
* @function initMiddlewares
* @memberOf NA#
* @this NA
*/
exports.initMiddlewares = function () {
var NA = this,
path = NA.modules.path,
compress = NA.modules.compress,
bodyParser = NA.modules.bodyParser,
cookieParser = NA.modules.cookieParser,
middlewares,
i,
l;
/* Use gzip and others client-server data compression. */
NA.express.use(compress());
/* Allow you to parse the x-www-form-urlencoded format. */
NA.express.use(bodyParser.urlencoded({ extended: true }));
/* Allow you to parse the JSON format. */
NA.express.use(bodyParser.json());
/* Allow you to parse the Cookie data format. */
NA.express.use(cookieParser());
function hasMiddlewaresFile(callback) {
if (
/**
* Allow you to set Express middleware for all routes.
* @public
* @alias middlewares
* @type {string}
* @memberOf NA#webconfig
*/
NA.webconfig.middlewares) {
middlewares = [];
if (NA.webconfig.middlewares instanceof Array) {
NA.webconfig.middlewares.forEach(function (middleware) {
callback(require(path.join(NA.serverPath, NA.webconfig.middlewaresRelativePath, middleware)).bind(NA));
});
} else {
callback(require(path.join(NA.serverPath, NA.webconfig.middlewaresRelativePath, NA.webconfig.middlewares)).bind(NA));
}
}
}
hasMiddlewaresFile(function (file) {
var content;
try {
content = file();
} catch (e) {
content = "";
}
if (content instanceof Array) {
middlewares = middlewares.concat(content);
} else {
middlewares.push(file);
}
});
if (middlewares) {
for (i = 0, l = middlewares.length; i < l; i++) {
NA.express.use(middlewares[i]);
}
}
};
/**
* Source Map for Less files.
* @private
* @function lessStoreSourcemap
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
* @param {RegExp} regex Used for change pathname.
*/
function lessStoreSourcemap(NA, regex) {
var fs = NA.modules.fs,
path = NA.modules.path,
mkpath = NA.modules.mkpath,
lessMiddlewareUtilities = NA.modules.lessMiddlewareUtilities;
return function (pathname, sourcemap) {
if (NA.webconfig.urlRelativeSubPath) {
pathname = pathname.replace(regex, path.join(NA.serverPath, NA.webconfig.assetsRelativePath) + path.sep);
}
fs.exists(path.join(pathname.replace(/\.css\.map/g,".less")), function (exists) {
if (exists) {
mkpath(path.dirname(pathname), function (error) {
if (error) {
lessMiddlewareUtilities.lessError(error);
return;
}
fs.writeFile(pathname, sourcemap, 'utf8', function () {});
});
}
});
};
}
/**
* CSS generate from Less files.
* @private
* @function lessStoreCss
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
* @param {RegExp} regex Used for change pathname.
*/
function lessStoreCss(NA, regex) {
var fs = NA.modules.fs,
path = NA.modules.path,
mkpath = NA.modules.mkpath;
return function (pathname, css, req, next) {
if (NA.webconfig.urlRelativeSubPath) {
pathname = pathname.replace(regex, path.join(NA.serverPath, NA.webconfig.assetsRelativePath) + path.sep);
}
mkpath(path.dirname(pathname), function (error) {
if (error) {
return next(error);
}
fs.writeFile(pathname, css, 'utf8', next);
});
};
}
/**
* Active Mechanism for generate Less files.
* @private
* @function initLessProcess
* @memberOf NA#
* @this NA
*/
exports.initLessProcess = function () {
var NA = this,
lessMiddleware = NA.modules.lessMiddleware,
path = NA.modules.path,
prefixLess = new NA.modules.prefixLess(),
compressValue = false,
sourceMapValue = true,
regex = new RegExp(path.join(NA.serverPath, NA.webconfig.assetsRelativePath, NA.webconfig.urlRelativeSubPath).replace(/(\\|\/)/g, '\\' + path.sep), 'g'),
renderOptions = {
compress: (NA.webconfig.less && NA.webconfig.less.compress) || compressValue,
sourceMap: (NA.webconfig.less && NA.webconfig.less.sourceMap) || sourceMapValue
};
if (NA.webconfig.less && NA.webconfig.less.autoprefix) {
renderOptions.plugins = [prefixLess];
}
if (NA.webconfig.less && !NA.webconfig.cssBundlingBeforeResponse) {
/* Generate Less on the fly during the development phase. */
NA.express.use(lessMiddleware(path.join(NA.webconfig.assetsRelativePath), {
dest: path.join(NA.webconfig.assetsRelativePath),
pathRoot: path.join(NA.serverPath),
preprocess: {
path: function (pathname) {
if (NA.webconfig.urlRelativeSubPath) {
pathname = pathname.replace(regex, path.join(NA.serverPath, NA.webconfig.assetsRelativePath) + path.sep);
}
return pathname;
}
},
postprocess: {
css: function (css, req) {
return css + "/*# sourceMappingURL=" + req.url.replace(/\.css$/i, '.css.map') + " */";
}
},
storeSourcemap: lessStoreSourcemap(NA, regex),
storeCss: lessStoreCss(NA, regex),
render: renderOptions
}));
}
};
/**
* Active Mechanism for generate Stylus files.
* @private
* @function initStylusProcess
* @memberOf NA#
* @this NA
*/
exports.initStylusProcess = function () {
var NA = this,
path = NA.modules.path,
stylusMiddleware,
compressValue = false,
sourceMapValue = {
"inline": true
},
forceValue = false,
firebugValue = false,
linenosValue = false;
if (NA.webconfig.stylus && !NA.webconfig.cssBundlingBeforeResponse) {
stylusMiddleware = NA.modules.stylus.middleware({
src: path.join(NA.serverPath, NA.webconfig.assetsRelativePath),
compile: function (str, src) {
var stylusFn = NA.modules.stylus(str);
if (NA.webconfig.stylus.autoprefix) {
stylusFn = stylusFn.use(NA.modules.prefixStylus());
}
stylusFn = stylusFn
.set('filename', src)
.set('src', path.join(NA.serverPath, NA.webconfig.assetsRelativePath))
.set('dest', path.join(NA.serverPath, NA.webconfig.assetsRelativePath))
.set('firebug', NA.webconfig.stylus.firebug || firebugValue)
.set('force', NA.webconfig.stylus.force || forceValue)
.set('linenos', NA.webconfig.stylus.linenos || linenosValue)
.set('sourcemap', NA.webconfig.stylus.sourceMap || sourceMapValue)
.set('compress', NA.webconfig.stylus.compress || compressValue);
return stylusFn;
}
});
/* Generate Stylus on the fly during the development phase. */
NA.express.use(function (req, res, next) {
var regex = new RegExp("^" + NA.webconfig.urlRelativeSubPath, 'g'),
request = {
url: req.url.replace(regex, ""),
method: req.method
};
stylusMiddleware(request, res, next);
});
}
};
/**
* Set the Sessions variables possibility.
* @private
* @function initSessions
* @memberOf NA#
* @this NA
* @param {NA~callback} next Passed to the next function.
* @param {NA~fallback} fallback Called if you want generate mode.
*/
exports.initSessions = function (next, fallback) {
var NA = this,
optionSession = {},
session = NA.modules.session;
/**
* Name for Session cookie of connected user.
* @public
* @alias sessionKey
* @type {string}
* @memberOf NA#webconfig
* @default "nodeatlas.sid"
*/
optionSession.key = NA.webconfig.sessionKey || "nodeatlas.sid";
/**
* Secret for Session cookie of connected user.
* @public
* @alias sessionSecret
* @type {string}
* @memberOf NA#webconfig
* @default '1234567890bépo'.
*/
optionSession.secret = NA.webconfig.sessionSecret || "1234567890bépo";
optionSession.saveUninitialized = true;
optionSession.resave = true;
if (NA.webconfig.session) {
/**
* Use a more complexe session cookie options.
* Replace `NA#webconfig.sessionKey` and `NA#webconfig.sessionSecret` if set.
* @public
* @alias session
* @type {Object}
* @memberOf NA#webconfig
* @see {@link https://github.com/expressjs/session Session Middleware}
*/
optionSession = NA.webconfig.session;
}
/**
* A default session loaded with `NA#webconfig.sessionKey` and `NA#webconfig.sessionSecret` or `NA.webconfig#sessionKey` and `NA#webconfig.session`.
* @public
* @alias sessionStore
* @type {Object}
* @memberOf NA#
* @see {@link https://github.com/expressjs/session Session Middleware}
*/
NA.sessionStore = new session.MemoryStore();
/* Use the `NA.controllers[<controller>].setSessions(...)` function if set... */
if (typeof NA.controllers[NA.webconfig.controller] !== 'undefined' &&
typeof NA.controllers[NA.webconfig.controller].setSessions !== 'undefined') {
/**
* Define this function for configure sessions of application. Only for `common` controller file.
* @function setSessions
* @memberOf NA#controllers[]
* @param {NA~callback} next Next steps after session is setted.
*/
NA.controllers[NA.webconfig.controller].setSessions.call(NA, function () {
NA.initSockets(session, optionSession, next, fallback);
});
/* ...else, just continue. */
} else {
NA.initSockets(session, optionSession, next, fallback);
}
};
/**
* Deliver NA.io to the client-side.
* @private
* @function addFrontSockets
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
*/
function addFrontSockets(NA) {
var fs = NA.modules.fs,
url = NA.modules.url,
path = NA.modules.path;
// Deliver the `NA.io` object to client-side.
if (NA.webconfig.socketClientFile) {
NA.express.get(url.format(path.join("/", NA.webconfig.urlRelativeSubPath, NA.webconfig.socketClientFile)), function (request, response) {
response.setHeader("Content-type", "text/javascript; charset=utf-8");
fs.readFile(path.join(NA.nodeatlasPath, "src", "socket.io.js"), "utf-8", function (err, content) {
if (err) {
throw err;
}
response.write(content
.replace(/%urlRelativeSubPath%/g, NA.webconfig.urlRelativeSubPath.slice(1))
.replace(/%urlRoot%/g, NA.webconfig.urlRoot));
response.end();
});
});
}
}
/**
* Set the Hooks for Controllors.
* @private
* @function addBackSockets
* @memberOf NA~
* @param {NA} NA NodeAtlas instance.
*/
function addBackSockets(NA) {
var path = NA.modules.path,
controllers = {},
controller;
/* Use the `NA.controllers[<controller>].setSockets(...)` function if set... */
if (typeof NA.controllers[NA.webconfig.controller] !== 'undefined' &&
typeof NA.controllers[NA.webconfig.controller].setSockets !== 'undefined') {
/**
* Define this function for set WebSockets from both `common` and `specific` controller.
* @function setSockets
* @memberOf NA#controllers[]
*/
NA.controllers[NA.webconfig.controller].setSockets.call(NA);
}
// Global sockets
NA.forEach(NA.webconfig.routes, function (route) {
if (typeof route === "string") {
route = NA.webconfig.routes[route];
}
if (route.controller) {
controllers[route.controller] = true;
}
});
// Controler setSockets runned just one time.
for (var item in controllers) {
if (controllers.hasOwnProperty(item)) {
/* Use the `NA.controllers[<controller>].setSockets(...)` function if set... */
controller = require(path.join(NA.serverPath, NA.webconfig.controllersRelativePath, item));
if (controller.setSockets) {
controller.setSockets.call(NA);
}
}
}
}
/**
* Allow you to set your websocket back-end behavior.
* @private
* @function initSockets
* @memberOf NA#
* @this NA
* @param {Object} session Session Object.
* @param {Object} optionSession Property for Object Session.
* @param {NA~callback} next Passed to the next function.
* @param {NA~fallback} fallback Called if you want generate mode.
*/
exports.initSockets = function (session, optionSession, next, fallback) {
var NA = this,
server = NA.httpsServer || NA.httpServer,
socketio = NA.modules.socketio,
secure = NA.webconfig.httpSecure ? true : false,
optionIo = (NA.webconfig.urlRelativeSubPath) ? {
path: NA.webconfig.urlRelativeSubPath + "/socket.io",
secure: secure
} : {
secure: secure
},
fullOptionIo = {};
Object.assign(fullOptionIo, optionIo,
/**
* Allow you to extend Socket.IO options object.
* @public
* @alias socketServerOptions
* @type {Object}
* @memberOf NA#webconfig
*/
NA.webconfig.socketServerOptions);
/**
* Instance of Socket.io module.
* @public
* @function express
* @memberOf NA#
*/
NA.io = socketio(server, fullOptionIo);
optionSession.store = NA.sessionStore;
NA.webconfig.session = optionSession;
/* Create a cookie Session. */
NA.express.use(session(optionSession));
/* Sync cookie Session with socket.io. */
NA.io.use(function (socket, next) {
session(optionSession)(socket.request, socket.request.res, next);
});
/* Deliver io to the client-side. */
addFrontSockets(NA);
/* Set Hooks for controllers. */
addBackSockets(NA);
/* Allow you to set configurations. */
NA.initConfigurations(next, fallback);
};
/**
* Allow you to configure your own modules configuration.
* @private
* @function initConfigurations
* @memberOf NA#
* @this NA
* @param {NA~callback} next Passed to the next function.
* @param {NA~fallback} fallback Called if you want generate mode.
*/
exports.initConfigurations = function (next, fallback) {
var NA = this;
/* Use the `NA.controllers[<controller>].setConfigurations(...)` function if set... */
if (typeof NA.controllers[NA.webconfig.controller] !== 'undefined' &&
typeof NA.controllers[NA.webconfig.controller].setConfigurations !== 'undefined') {
/**
* Define this function for configure all modules of your application. Only for `common` controller file.
* @function setConfigurations
* @memberOf NA#controllers[]
* @param {NA~callback} next Next steps after configuration is done.
*/
NA.controllers[NA.webconfig.controller].setConfigurations.call(NA, function () {
NA.initServer(next, fallback);
});
/* ...else, just continue. */
} else {
NA.initServer(next, fallback);
}
};
/**
* Run the Server of NodeAtlas.
* @private
* @function initServer
* @memberOf NA#
* @this NA
* @param {NA~callback} next Called after running of server.
* @param {NA~fallback} fallback Called if you want generate mode.
*/
exports.initServer = function (next, fallback) {
var NA = this,
opn = NA.modules.opn,
path = NA.modules.path,
url = NA.modules.url,
urlOutput = url.resolve(NA.webconfig.urlRoot, path.join(NA.webconfig.urlRelativeSubPath, ((typeof NA.configuration.browse === 'string') ? NA.configuration.browse : "")));
NA.express.set("port", NA.webconfig.httpPort);
if (!NA.configuration.generate) {
serverListener(NA, NA.webconfig.httpPort, urlOutput, "isRunning", function () {
/* If index index exist, we go to url later. */
if (NA.configuration.browse && !NA.webconfig.index) {
opn(urlOutput);
}
next();
});
} else {
fallback();
}
/* Catche error from Server. */
serverError(NA);
};