UNPKG

ember-cli-ajh

Version:

Command line tool for developing ambitious ember.js apps

361 lines (330 loc) 10.1 kB
/* server.js ========= Testem's server. Serves up the HTML, JS, and CSS required for running the tests in a browser. */ var express = require('express') var socketIO = require('socket.io-pure') var fs = require('fs') var path = require('path') var async = require('async') var log = require('npmlog') var EventEmitter = require('events').EventEmitter var path = require('path') var Mustache = require('consolidate').mustache var http = require('http') var https = require('https') var httpProxy = require('http-proxy') function Server(config){ this.config = config this.ieCompatMode = null // Maintain a hash of all connected sockets to close them manually // Workaround https://github.com/joyent/node/issues/9066 this.sockets = {} this.nextSocketId = 0 } Server.prototype = { __proto__: EventEmitter.prototype, start: function(callback){ callback = callback || function(){} this.createExpress() var self = this // Start the server! // Create socket.io sockets this.server.on('listening', function() { self.config.set('port', self.server.address().port) callback(null) self.emit('server-start') }); this.server.on('error', function(e) { self.stopped = true callback(e) self.emit('server-error', e) }); this.server.on('connection', function (socket) { var socketId = self.nextSocketId++ self.sockets[socketId] = socket socket.on('close', function () { delete self.sockets[socketId]; }); }); this.server.listen(this.config.get('port')) }, stop: function(callback){ if (this.server && !this.stopped) { this.stopped = true this.server.close(callback) // Destroy all open sockets for (var socketId in this.sockets) { this.sockets[socketId].destroy(); } } else { callback() } }, createExpress: function(){ var self = this var app = this.express = express() if(this.config.get('key') || this.config.get('pfx')) { var options = {} if (this.config.get('key')) { options.key = fs.readFileSync(this.config.get('key')) options.cert = fs.readFileSync(this.config.get('cert')) } else { options.pfx = fs.readFileSync(this.config.get('pfx')) } this.server = https.createServer(options, this.express) } else { this.server = http.createServer(this.express) } this.io = socketIO(this.server) this.io.on('connection', this.onClientConnected.bind(this)) this.configureExpress(app) this.injectMiddleware(app) this.configureProxy(app) app.get('/', function(req, res){ self.serveHomePage(req, res) }) app.get(/\/([0-9]+)$/, function(req, res){ self.serveHomePage(req, res) }) app.get('/testem.js', function(req, res){ self.serveTestemClientJs(req, res) }) app.get(/^\/(?:[0-9]+)(\/.+)$/, serveStaticFile) app.post(/^\/(?:[0-9]+)(\/.+)$/, serveStaticFile) app.get(/^(.+)$/, serveStaticFile) app.post(/^(.+)$/, serveStaticFile) app.use(function(err, req, res, next){ if (err){ log.error(err.message) if (err.code === 'ENOENT'){ res.status(404).send('Not found: ' + req.url) }else{ res.status(500).send(err.message) } }else{ next() } }) function serveStaticFile(req, res){ self.serveStaticFile(req.params[0], req, res) } }, configureExpress: function(app){ var self = this app.engine('mustache', Mustache) app.set("view options", {layout: false}) app.use(function(req, res, next){ if (self.ieCompatMode) res.setHeader('X-UA-Compatible', 'IE=' + self.ieCompatMode) next() }) app.use(express.static(__dirname + '/../../public')) }, injectMiddleware: function(app) { var middlewares = this.config.get('middleware') if (middlewares) { middlewares.forEach(function(middleware) { middleware(app); }) } }, shouldProxy: function(req, opts) { var accepts, acceptCheck = [ 'html', 'css', 'javascript', ]; //Only apply filtering logic if 'onlyContentTypes' key is present if (!('onlyContentTypes' in opts)) { return true; } acceptCheck = acceptCheck.concat(opts.onlyContentTypes) acceptCheck.push('text') accepts = req.accepts(acceptCheck); if (accepts.indexOf(opts.onlyContentTypes) !== -1) { return true; } return false; }, configureProxy: function(app) { var proxies = this.config.get('proxies'); var self = this; if (proxies) { self.proxy = new httpProxy.createProxyServer({changeOrigin: true}) Object.keys(proxies).forEach(function(url) { app.all(url + '*', function(req, res, next) { var opts = proxies[url]; if (self.shouldProxy(req, opts)) { if (opts.host) { opts.target = 'http://' + opts.host + ':' + opts.port delete opts.host delete opts.port } self.proxy.web(req, res, opts) } else { next() } }) }); } }, renderRunnerPage: function(err, files, res){ var config = this.config var framework = config.get('framework') || 'jasmine' var css_files = config.get('css_files') var templateFile = { jasmine: 'jasminerunner', jasmine2: 'jasmine2runner', qunit: 'qunitrunner', mocha: 'mocharunner', 'mocha+chai': 'mochachairunner', buster: 'busterrunner', custom: 'customrunner', tap: 'taprunner' }[framework] + '.mustache' res.render(__dirname + '/../../views/' + templateFile, { scripts: files, styles: css_files }) }, renderDefaultTestPage: function(req, res){ res.header('Cache-Control', 'No-cache') res.header('Pragma', 'No-cache') var self = this var config = this.config var test_page = config.get('test_page') if (test_page){ if (test_page[0] === "/") { test_page = encodeURIComponent(test_page) } var base = req.path === '/' ? req.path : req.path + '/' var url = base + test_page res.redirect(url) } else { config.getServeFiles(function(err, files){ self.renderRunnerPage(err, files, res) }) } }, serveHomePage: function(req, res){ var config = this.config var routes = config.get('routes') || config.get('route') || {} if (routes['/']){ this.serveStaticFile('/', req, res) }else{ this.renderDefaultTestPage(req, res) } }, serveTestemClientJs: function(req, res){ res.setHeader('Content-Type', 'text/javascript') res.write(';(function(){') var files = [ 'decycle.js', 'jasmine_adapter.js', 'jasmine2_adapter.js', 'qunit_adapter.js', 'mocha_adapter.js', 'buster_adapter.js', 'testem_client.js' ] async.forEachSeries(files, function(file, done){ if (file.indexOf(path.sep) === -1) { file = __dirname + '/../../public/testem/' + file } fs.readFile(file, function(err, data){ if (err){ res.write('// Error reading ' + file + ': ' + err) }else{ res.write('\n//============== ' + path.basename(file) + ' ==================\n\n') res.write(data) } done() }) }, function(){ res.write('}());') res.end() }) }, killTheCache: function killTheCache(req, res){ res.setHeader('Cache-Control', 'No-cache') res.setHeader('Pragma', 'No-cache') delete req.headers['if-modified-since'] delete req.headers['if-none-match'] }, route: function route(uri){ var config = this.config var routes = config.get('routes') || config.get('route') || {} var bestMatchLength = 0 var bestMatch = null for (var prefix in routes){ if (uri.substring(0, prefix.length) === prefix){ if (bestMatchLength < prefix.length){ if (routes[prefix] instanceof Array) { routes[prefix].some(function(folder) { bestMatch = folder + '/' + uri.substring(prefix.length) return fs.existsSync(config.resolvePath(bestMatch)) }) } else { bestMatch = routes[prefix] + '/' + uri.substring(prefix.length) } bestMatchLength = prefix.length } } } return { routed: !!bestMatch, uri: bestMatch || uri.substring(1) } }, serveStaticFile: function(uri, req, res){ var self = this var config = this.config var routeRes = this.route(uri) uri = routeRes.uri var wasRouted = routeRes.routed this.killTheCache(req, res) var allowUnsafeDirs = config.get('unsafe_file_serving') var filePath = path.resolve(config.resolvePath(uri)) var ext = path.extname(filePath) var isPathPermitted = filePath.indexOf(config.cwd()) !== -1 if (!wasRouted && !allowUnsafeDirs && !isPathPermitted) { res.status(403) res.write('403 Forbidden') res.end() } else if (ext === '.mustache') { config.getTemplateData(function(err, data){ res.render(filePath, data) self.emit('file-requested', filePath) }) } else { fs.stat(filePath, function(err, stat){ self.emit('file-requested', filePath) if (err) return res.sendFile(filePath) if (stat.isDirectory()){ fs.readdir(filePath, function(err, files){ var dirListingPage = __dirname + '/../../views/directorylisting.mustache' res.render(dirListingPage, {files: files}) }) }else{ res.sendFile(filePath) } }) } }, onClientConnected: function(client){ var self = this client.once('browser-login', function(browserName, id){ log.info('New client connected: ' + browserName + ' ' + id) self.emit('browser-login', browserName, id, client) }) } } module.exports = Server