UNPKG

expresser

Version:

A ready to use Node.js web app wrapper, built on top of Express.

485 lines (386 loc) 17.9 kB
# EXPRESSER APP # ----------------------------------------------------------------------------- express = require "express" errors = require "./errors.coffee" events = require "./events.coffee" fs = require "fs" http = require "http" https = require "https" lodash = require "lodash" logger = require "./logger.coffee" path = require "path" settings = require "./settings.coffee" util = require "util" utils = require "./utils.coffee" nodeEnv = null ### # This is the "core" of an Expresser based application. The App wraps the # Express server and its middlewares / routes, along with a few extra helpers. ### class App newInstance: -> return new App() ## # Exposes the Express app object to the outside. # @property # @type express-Application expressApp: null ## # The underlying HTTP(S) server. # @property # @type http-Server webServer: null ## # The HTTP to HTTPS redirector server (only if settings.app.ssl.redirectorPort is set). # @property # @type http-Server redirectorServer: null ## # Additional middlewares to be used by the Express server. # These will be called before the default middlewares. # @property # @type Array prependMiddlewares: [] ## # Additional middlewares to be used by the Express server. # These will be called after the default middlewares. # @property # @type Array appendMiddlewares: [] # INIT # -------------------------------------------------------------------------- ### # Create, configure and run the Express application. In most cases this should be # the last step of you app loading, after loading custom modules, setting custom # configuration, etc. Please note that this will be called automatically if # you call the main `expresser.init()`. ### init: -> logger.debug "App.init" events.emit "App.before.init" nodeEnv = process.env.NODE_ENV # Get version from main app's package.json (NOT Expresser, but the actual app using it!) try if @expresser?.rootPath @version = require(@expresser.rootPath + "/package.json")?.version else @version = require(__dirname + "../../package.json")?.version catch ex logger.error "App.init", "Could not fetch version from package.json.", ex # Configure the Express server. @configure() # Start web server! @start() events.emit "App.on.init" delete @init ### # Configure the server. Set views, options, use Express modules, etc. # Called automatically on `init()`, so normally you should never need # to call `configure()` on your own. # @private ### configure: -> midBodyParser = require "body-parser" midCookieParser = require "cookie-parser" midCompression = require "compression" midSession = require "express-session" memoryStore = require("memorystore") midSession if settings.general.debug or nodeEnv is "test" midErrorHandler = require "errorhandler" # Create express v4 app. @expressApp = express() # DEPRECATED! The "server" was renamed to "expressApp". @server = @expressApp # BRAKING! Alert if user is still using old ./views default path for views. if not fs.existsSync(settings.app.viewPath) logger.warn "Attention!", "Views path not found: #{settings.app.viewPath}", "Note that the default path has changed from ./views/ to ./assets/views/" # Set view options, use Pug for HTML templates. @expressApp.set "views", settings.app.viewPath @expressApp.set "view engine", settings.app.viewEngine @expressApp.set "view options", { layout: false } # Prepend middlewares, if any was specified. if @prependMiddlewares.length > 0 @expressApp.use mw for mw in @prependMiddlewares # Use Express basic handlers. @expressApp.use midBodyParser.json {limit: settings.app.bodyParser.limit} @expressApp.use midBodyParser.urlencoded {extended: settings.app.bodyParser.extended, limit: settings.app.bodyParser.limit} if settings.app.cookie.enabled @expressApp.use midCookieParser settings.app.cookie.secret if settings.app.session.enabled @expressApp.use midSession {store: new memoryStore(), secret: settings.app.session.secret, resave: false, saveUninitialized: false, cookie: {httpOnly: settings.app.session.httpOnly, maxAge: new Date(Date.now() + (settings.app.session.maxAge * 1000))}} # Use HTTP compression only if enabled on settings. if settings.app.compressionEnabled @expressApp.use midCompression # Fix connect assets helper context. connectAssetsOptions = lodash.cloneDeep settings.app.connectAssets connectAssetsOptions.helperContext = @expressApp.locals # Connect assets and dynamic compiling. ConnectAssets = (require "./app/connect-assets.js") connectAssetsOptions @expressApp.use ConnectAssets # Append extra middlewares, if any was specified. if @appendMiddlewares.length > 0 @expressApp.use mw for mw in @appendMiddlewares # Configure development environment to dump exceptions and show stack. if settings.general.debug or nodeEnv is "test" @expressApp.use midErrorHandler {dumpExceptions: true, showStack: true} # Use Express static routing. @expressApp.use express.static settings.app.publicPath # Log all requests if debug is true. if settings.general.debug @expressApp.use (req, res, next) -> ip = utils.browser.getClientIP req method = req.method url = req.url console.log "Request from #{ip}", method, url next() if next? return url # We should not call configure more than once! delete @configure # START AND KILL # -------------------------------------------------------------------------- ### # Start the server using HTTP or HTTPS, depending on the settings. ### start: -> if @webServer? return logger.warn "App.start", "Application has already started (webServer is not null). Abort!" events.emit "App.before.start" if settings.app.ssl.enabled and settings.app.ssl.keyFile? and settings.app.ssl.certFile? sslKeyFile = utils.io.getFilePath settings.app.ssl.keyFile sslCertFile = utils.io.getFilePath settings.app.ssl.certFile # Certificate files were found? Proceed, otherwise alert the user and throw an error. if sslKeyFile? and sslCertFile? if fs.existsSync(sslKeyFile) and fs.existsSync(sslCertFile) sslKey = fs.readFileSync sslKeyFile, {encoding: settings.general.encoding} sslCert = fs.readFileSync sslCertFile, {encoding: settings.general.encoding} sslOptions = {key: sslKey, cert: sslCert} serverRef = https.createServer sslOptions, @expressApp else return errors.throw "certificatesNotFound", "Please check paths defined on settings.app.ssl." else return errors.throw "certificatesNotFound", "Please check paths defined on settings.app.ssl." else serverRef = http.createServer @expressApp # Expose the web server. @webServer = serverRef # Start the app! if settings.app.ip? and settings.app.ip isnt "" serverRef.listen settings.app.port, settings.app.ip logger.info "App", settings.app.title, "Listening on #{settings.app.ip} port #{settings.app.port}" else serverRef.listen settings.app.port logger.info "App", settings.app.title, "Listening on port #{settings.app.port}" # Using SSL and redirector port is set? Then create the http server. if settings.app.ssl.enabled and settings.app.ssl.redirectorPort > 0 logger.info "App", "#{settings.app.title} will redirect HTTP #{settings.app.ssl.redirectorPort} to HTTPS on #{settings.app.port}." redirServer = express() redirServer.get "*", (req, res) -> res.redirect "https://#{req.hostname}:#{settings.app.port}#{req.url}" # Log all redirector requests if debug is true. if settings.general.debug redirServer.use @requestLogger @redirectorServer = http.createServer redirServer @redirectorServer.listen settings.app.ssl.redirectorPort # Pass the HTTP(s) server created to external modules. events.emit "App.on.start", serverRef ### # Kill the underlying HTTP(S) server(s). ### kill: -> events.emit "App.before.kill" try @webServer?.close() @redirectorServer?.close() catch ex logger.error "App.kill", ex webServer = null @redirectorServer = null events.emit "App.on.kill" # BRIDGED EXPRESS METHODS # -------------------------------------------------------------------------- ## # Helper to call the Express App .all(). all: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.all", util.inspect(arguments[0]), util.inspect(arguments[1]) @expressApp.all.apply @expressApp, arguments ## # Helper to call the Express App .get(). get: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.get", util.inspect(arguments[0]), util.inspect(arguments[1]) @expressApp.get.apply @expressApp, arguments ## # Helper to call the Express App .post(). post: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.post", util.inspect(arguments[0]), util.inspect(arguments[1]) @expressApp.post.apply @expressApp, arguments ## # Helper to call the Express App .put(). put: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.put", util.inspect(arguments[0]), util.inspect(arguments[1]) @expressApp.put.apply @expressApp, arguments ## # Helper to call the Express App .patch(). patch: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.patch", util.inspect(arguments[0]), util.inspect(arguments[1]) @expressApp.patch.apply @expressApp, arguments ## # Helper to call the Express App .delete(). delete: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.delete", util.inspect(arguments[0]), util.inspect(arguments[1]) @expressApp.delete.apply @expressApp, arguments ## # Helper to call the Express App .listen(). listen: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.listen", util.inspect(arguments[0]), util.inspect(arguments[1]) @expressApp.listen.apply @expressApp, arguments ## # Helper to call the Express App .route(). route: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.route", arguments[0] @expressApp.route.apply @expressApp, arguments ## # Helper to call the Express App .use(). use: => return errors.throw "expressNotInit" if not @expressApp? logger.debug "App.use", util.inspect(arguments[0]), util.inspect(arguments[1]) @expressApp.use.apply @expressApp, arguments # HELPER AND UTILS # -------------------------------------------------------------------------- ### # Return an array with all routes registered on the Express application. # @param {Boolean} asString If true, returns the route strings only, otherwise returns full objects. # @return {Array} Array with the routes (as object or as string if asString = true). ### listRoutes: (asString = false) => result = [] for r in @expressApp._router.stack if r.route?.path? and r.route.path isnt "" if asString result.push r.route.path else result.push {route: r.route.path, methods: lodash.keys(r.route.methods)} return result ### # Render a Pug view and send to the client. # @param {Object} req The Express request object, mandatory. # @param {Object} res The Express response object, mandatory. # @param {String} view The Pug view filename, mandatory. # @param {Object} options Options passed to the view, optional. ### renderView: (req, res, view, options) -> logger.debug "App.renderView", req.originalUrl, view, options try options = {} if not options? options.device = utils.browser.getDeviceDetails req options.title = settings.app.title if not options.title? # View filename must jave .pug extension. view += ".pug" if view.indexOf(".pug") < 0 # Send rendered view to client. res.render view, options catch ex logger.error "App.renderView", view, ex @renderError req, res, ex events.emit "App.on.renderView", req, res, view, options ### # Render response as JSON data and send to the client. # @param {Object} req The Express request object, mandatory. # @param {Object} res The Express response object, mandatory. # @param {Object} data The JSON data to be sent, mandatory. ### renderJson: (req, res, data) -> logger.debug "App.renderJson", req.originalUrl, data if lodash.isString data try data = JSON.parse data catch ex return @renderError req, res, ex, 500 # Remove methods from JSON before rendering. cleanJson = (obj, depth) -> if depth > settings.logger.maxDepth return if lodash.isArray obj for i in obj cleanJson i, depth + 1 else if lodash.isObject obj for k, v of obj if lodash.isFunction v delete obj[k] else cleanJson v, depth + 1 cleanJson data, 0 # Add Access-Control-Allow-Origin to all when debug is true. if settings.general.debug res.setHeader "Access-Control-Allow-Origin", "*" # Send JSON response. res.json data events.emit "App.on.renderJson", req, res, data ### # Render an image from the speficied file, and send to the client. # @param {Object} req The Express request object, mandatory. # @param {Object} res The Express response object, mandatory. # @param {String} filename The full path to the image file, mandatory. # @param {Object} options Options passed to the image renderer, for example the "mimetype". ### renderImage: (req, res, filename, options) -> logger.debug "App.renderImage", req.originalUrl, filename, options mimetype = options?.mimetype # Try to figure out the mime type in case it wasn't passed along the options. if not mimetype? extname = path.extname(filename).toLowerCase().replace(".","") extname = "jpeg" if extname is "jpg" mimetype = "image/#{extname}" # Send image to client. res.contentType mimetype res.sendFile filename events.emit "App.on.renderImage", req, res, filename, options ### # Sends error response as JSON. # @param {Object} req The Express request object, mandatory. # @param {Object} res The Express response object, mandatory. # @param {Object} error The error object or message to be sent to the client, mandatory. # @param {Number} status The response status code, optional, default is 500. ### renderError: (req, res, error, status) -> logger.debug "App.renderError", req.originalUrl, status, error # Status defaults to 500. status = error?.statusCode or 500 if not status? status = 408 if status is "ETIMEDOUT" # Helper to build message out of the error object. getMessage = (obj) -> msg = {} if lodash.isString obj msg.message = obj else msg.message = obj.message if obj.message? msg.friendlyMessage = obj.friendlyMessage if obj.friendlyMessage? msg.reason = obj.reason if obj.reason? msg.code = obj.code if obj.code? # Nothing taken out of error objec? Return null then. if lodash.keys(msg).length is 0 return null return msg try message = getMessage error # Error might be encapsulated inside another "error" property. if not mesage? and error.error? message = getMessage error.error catch ex logger.error "App.renderError", ex # Can't figure it out? Just pass error as string then. if not message? message = error.toString() # Send error JSON to client. res.status(status).json {error: message, url: req.originalUrl} events.emit "App.on.renderError", req, res, error, status # Singleton implementation # ----------------------------------------------------------------------------- App.getInstance = -> @instance = new App() if not @instance? return @instance module.exports = App.getInstance()