UNPKG

katana

Version:

Easy to use, modular web framework for any Node.js samurai.

522 lines (391 loc) 13.1 kB
var Http = require('http'); var Https = require('https'); var Domain = require('domain'); var Path = require('path'); var Fs = require('fs'); var EventEmitter = require('events').EventEmitter; var Async = require('async'); var _ = require('underscore'); var Url = require('url'); var parseQuery = require('qs').parse; var find = require('./fs').find; var utils = require('./utils'); var sanitize = utils.sanitize; var merge = utils.merge; var map = Object.map; var isWin = process.platform === 'win32'; var eol = require('os').EOL; var sep = Path.sep; module.exports = global.App = new EventEmitter; App.setMaxListeners(0); App.version = require('../package.json').version; App.env = process.env.NODE_ENV || 'development'; App.root = Path.dirname(process.mainModule.filename) + Path.sep; App.info = require(App.root + 'package.json'); process.chdir(App.root); App.middlewares = {}; App.settings = {}; App._modules = ['app'].concat( Object.keys(App.info.katana.modules).filter(function(name) { return App.info.katana.modules[name].enabled; }) ); App.resolve = function(path) { var module = 'app'; var match = null; path = path.trim().replace(/\s+|\./g, '/'); if (match = path.match(/(.*)\:(.*)/)) { module = match[1]; path = match[2].trim(); } return module +'/'+ path; } function resolveKey(path) { var module = 'app'; var conf = 'app'; var match = null; if (match = path.match(/(\w+)?\:([^\.\s]+)?(?:[\.\s]*)?(.*)?/)) { module = match[1] || module; conf = match[2] || (module==='app' ? conf : module); path = match[3] || ''; } path = path.trim().replace(/\s+|\./g, '/'); return (module +'/'+ conf + (path ? '/'+ path : '')); } App.get = function(key) { if (!key) { return this.settings; } var path = resolveKey(key).split('/'); var target = this.settings[path.shift()]; if (!path.length) { return target; } while(path.length > 0) { key = path.shift(); if (target && target.hasOwnProperty(key)) { target = target[key]; continue; } return undefined; } return target; } App.set = function(key, value) { var path = resolveKey(key).split('/'); var module = path.shift(); var target = this.settings[module]; if (!target) { target = this.settings[module] = {}; } if (!path.length) { return false; } while(path.length > 1) { key = path.shift(); if (typeof(target[key]) !== 'object') { target[key] = {}; } target = target[key]; } key = path.shift(); target[key] = value; return true; } App.enable = function(key) { this.set(key, true); } App.disable = function(key) { this.set(key, false); } App.enabled = function(key) { return !!this.get(key); } App.disabled = function(key) { return !this.get(key); } App.readConf = function(env) { var self = this; env = env || 'development'; self._modules.forEach(function(module) { var dir = self.root + (module==='app' ? 'app' : 'modules/'+ module) +'/config/'; if (Fs.existsSync(dir + env) && Fs.statSync(dir + env).isDirectory()) { Fs.readdirSync(dir + env).forEach(function(file) { if (Path.extname(file) !== '.js' && Path.extname(file) !== '.json') { return; } var name = Path.basename(file, Path.extname(file)); var conf = require(dir + env +'/'+ file); self.set(module +':'+ name, (env==='development' ? conf : merge(self.get(module +':'+ name), conf))); }); } }); } App.readConf(); (App.env !== 'development') && App.readConf(App.env); require('./cli-args'); App.logger = require('./logger'); App.log = function() { if (arguments[0] instanceof Error) { arguments[1] = arguments[0].toString(); arguments[0] = 'error'; } else if (arguments[1] instanceof Error) { arguments[1] = arguments[1].toString(); } App.logger.log.apply(App.logger, [].slice.call(arguments)); } var Request = require('./request'); var Response = require('./response'); App.start = function() { var http = this.get('http'); var https = this.get('https'); var wait = !!http + !!https; !wait && App.emit('start'); if (http) { var httpHost = http.host || '127.0.0.1'; var httpPort = http.port || 8000; this.http = Http.createServer(wrapRequest); this.http.listen(httpPort, httpHost, function() { App.log('info', 'Katana HTTP server on: '+ httpHost +':'+ httpPort); (!--wait) && App.emit('start'); }); } if (https) { var httpsHost = https.host || (http && http.host) || '127.0.0.1'; var httpsPort = https.port || 443; this.https = Https.createServer({ key: https.key || null, cert: https.cert || null, ca: https.ca || null }, wrapRequest); this.https.listen(httpsPort, httpsHost, function() { App.log('info', 'Katana HTTPS server on: '+ httpsHost +':'+ httpsPort); (!--wait) && App.emit('start'); }); } } function wrapRequest(request, response) { var domain = Domain.create(); domain.on('error', function(error) { if (response.headersSent) { return response.end(); } response.send(500); }); domain.add(request); domain.add(response); domain.run(function() { App.handleRequest(request, response); }); } var staticsEnabled = App.get('statics enabled'); if (staticsEnabled) { var conf = App.get('statics'); var staticsPath = (conf.path || 'public') + '/'; var Static = new (require('node-static').Server)({ cache: conf.cache || 7200, serverInfo: conf.serverInfo || ('Katana v'+ App.version), headers: conf.headers, gzip: conf.gzip }); } App.handleRequest = function(request, response) { var self = this; if (request.url === '/favicon.ico') { request.url = self.get('favicon') || 'public/images/favicon.ico'; } request.__proto__ = Request; response.__proto__ = Response; request.response = response; response.request = request; request.originalUrl = request.url; request.url = utils.decodeURI(request.url); request.parsedUrl = Url.parse(request.url); request.path = sanitize(request.parsedUrl.pathname).trim('\\s\/'); request.targetPath = Path.normalize(request.path) + '/'; request.query = request.parsedUrl.query ? parseQuery(request.parsedUrl.query) : {}; request.data = {}; request.files = {}; if (isWin) { request.targetPath = request.targetPath.replace(/\\/g, '/'); } if (staticsEnabled && request.targetPath.indexOf(staticsPath) === 0) { return Static.serve(request, response); } request.route = self.router.route(request.path, request.method); if (request.module !== 'app') { // var params = request.route.params; // request.originalRoute = request.route; request.route = self.router.route(request.route.routed, request.method, request.module); // route.params = merge(params, route.params); } self.callMiddlewares('app:request', request, response, function(error) { if (error) { // return response.send(error); response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end(error); return; } self.handle(request, response); }); } var controller404 = App.get(':routing _404 controller') || 'home'; var action404 = App.get(':routing _404 action') || '_404'; App.handle = function(request, response) { var self = this; var controller = self.controllers[request.module +'/'+ request.directory + request.controller]; var _404 = false; if (!controller) { controller = self.controllers['app/'+ controller404]; _404 = true; } if (controller) { var method = _404 ? controller[action404] : (controller[request.action] || controller._call || controller[action404]); if (typeof(method) === 'function') { method = method.bind(controller); method(request, response); } else { response.send(404, 'action not found'); } } else { response.send(404, 'controller not found'); } } App.use = function(type, fn) { if (typeof(type) === 'function') { fn = type; type = 'app:request'; } (this.middlewares[type] || (this.middlewares[type] = [])).push(fn); } App.callMiddlewares = function(type, done) { var args = [].slice.call(arguments); var self = this; var index = 0; var type = args[0]; var done = args[args.length - 1]; if (!(this.middlewares[type] || (this.middlewares[type] = [])).length) { return done(); } function next(error) { if (error) { return done(error); } if ((self.middlewares[type].length) <= index++) { return done(); } var fn = self.middlewares[type][index-1]; fn.apply(fn, args); } args = args.slice(1, -1).concat(next); next(); } App.call = function(path) { var segments = path.split('/'); var action = segments.pop(); var controller = this.controller(segments.join('/')); var args = [].slice.call(arguments, 1); if (!controller || typeof(controller[action])!=='function') { var callback = typeof(args[args.length-1])==='function' ? args[args.length-1] : function(error) { throw error; } return callback(new Error('controller '+ segments.join('/') +' has no method '+ action)); } controller[action].apply(controller, args); } setImmediate(function() { Async.series({ stores: function(next) { App.callMiddlewares('before:stores', function(error) { if (error) { return next(error); } var stores = App.get(':stores'); stores = map(stores, function(name, options) { var fn = function(next) { App.store.connect(this.provider, this, next); }; return [name, fn.bind(options)]; }); Async.parallel(stores, function(error, connections) { if (error) { return next(error); } App.store.stores = connections; App.callMiddlewares('after:stores', next); }); }); }, models: function(next) { App.callMiddlewares('before:models', function(error) { if (error) { return next(error); } importFiles('models', function(error) { if (error) { return next(error); } App.callMiddlewares('after:models', next); }); }); }, views: function(next) { App.callMiddlewares('before:views', function(error) { if (error) { return next(error); } importFiles('views', function(filePath) { return Fs.readFileSync(filePath, 'utf-8'); }, function(error) { if (error) { return next(error); } App.callMiddlewares('after:views', next); }); }); }, modules: function(next) { App.modules = {}; App.callMiddlewares('before:modules', function(error) { if (error) { return next(error); } App._modules.forEach(function(module) { if (module !== 'app') { App.modules[module] = require(App.root +'modules/'+ module); } }); App.callMiddlewares('after:modules', next); }); }, controllers: function(next) { App.callMiddlewares('before:controllers', function(error) { if (error) { return next(error); } importFiles('controllers', function(error) { if (error) { return next(error); } App.callMiddlewares('after:controllers', next); }); }); } }, function(error) { if (error) { App.log('error', error); process.exit(1); } App.callMiddlewares('before:start', App.start.bind(App)); }); }); App.model = function(path) { return App.models[this.resolve(path)]; } App.controller = function(path) { return App.controllers[this.resolve(path)]; } App.module = function(name) { return (name ? this.modules[name] : this.modules); } function importFiles(type, fn, next) { if (arguments.length < 3) { next = fn; fn = function(file) { return require(file); } } Async.each(App._modules, function(module, cb) { var path = App.root + (module === 'app' ? ['app', type] : ['modules', module, type]).join(sep); find(path, function(error, files) { if (error) { return cb(error); } var items = {}; files.forEach(function(file) { var dir = Path.dirname(Path.relative(path, file)); dir==='.' && (dir=''); var name = Path.basename(file, Path.extname(file)); var itemPath = dir ? dir +'/'+ name : name; items[itemPath] = fn(file, itemPath, dir, name); }); _.extend(App[type] || (App[type] = {}), map(items, function(name, item) { return [module +'/'+ name, item]; })); cb(); }); }, next); } App.utils = utils; App.view = require('./view'); App.render = App.view.render.bind(App.view); App.router = require('./router'); App.store = require('./store'); App.Controller = require('./controller'); App.get('cookies enabled') && require('./cookies'); App.get('session enabled') && require('./session'); App.get('multiparser enabled') && require('./multiparser');