Skip to content

Commit

Permalink
refactor(web-server): a little refactoring, use connect ;-)
Browse files Browse the repository at this point in the history
The architecture was actually pretty similar to connect, so this refactoring could have been a very simple change. Unfortunately I decided to clean things up a little bit…

Closes #105
  • Loading branch information
vojtajina committed Jul 25, 2013
1 parent 0d22919 commit 7255aa6
Show file tree
Hide file tree
Showing 13 changed files with 778 additions and 642 deletions.
70 changes: 70 additions & 0 deletions lib/middleware/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* This module contains some common helpers shared between middlewares
*/

var mime = require('mime');
var log = require('../logger').create('web-server');

var PromiseContainer = function() {
var promise;

this.then = function(success, error) {
return promise.then(success, error);
};

this.set = function(newPromise) {
promise = newPromise;
};
};


var serve404 = function(response, path) {
log.warn('404: ' + path);
response.writeHead(404);
return response.end('NOT FOUND');
};


var createServeFile = function(fs, directory) {
return function(filepath, response, transform) {
if (directory) {
filepath = directory + filepath;
}

return fs.readFile(filepath, function(error, data) {
if (error) {
return serve404(response, filepath);
}

response.setHeader('Content-Type', mime.lookup(filepath, 'text/plain'));

// call custom transform fn to transform the data
var responseData = transform && transform(data.toString()) || data;

response.writeHead(200);

log.debug('serving: ' + filepath);
return response.end(responseData);
});
};
};


var setNoCacheHeaders = function(response) {
response.setHeader('Cache-Control', 'no-cache');
response.setHeader('Pragma', 'no-cache');
response.setHeader('Expires', (new Date(0)).toString());
};


var setHeavyCacheHeaders = function(response) {
response.setHeader('Cache-Control', ['public', 'max-age=31536000']);
};


// PUBLIC API
exports.PromiseContainer = PromiseContainer;
exports.createServeFile = createServeFile;
exports.setNoCacheHeaders = setNoCacheHeaders;
exports.setHeavyCacheHeaders = setHeavyCacheHeaders;
exports.serve404 = serve404;
117 changes: 117 additions & 0 deletions lib/middleware/karma.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Karma middleware is responsible for serving:
* - client.html (the entrypoint for capturing a browser)
* - debug.html
* - context.html (the execution context, loaded within an iframe)
* - karma.js
*
* The main part is generating context.html, as it contains:
* - generating mappings
* - including <script> and <link> tags
* - setting propert caching headers
*/

var path = require('path');
var util = require('util');

var common = require('./common');

var VERSION = require('../constants').VERSION;
var SCRIPT_TAG = '<script type="%s" src="%s"></script>';
var LINK_TAG = '<link type="text/css" href="%s" rel="stylesheet">';
var SCRIPT_TYPE = {
'.js': 'text/javascript',
'.dart': 'application/dart'
};


var filePathToUrlPath = function(filePath, basePath) {
if (filePath.indexOf(basePath) === 0) {
return '/base' + filePath.substr(basePath.length);
}

return '/absolute' + filePath;
};

var createKarmaMiddleware = function(filesPromise, serveStaticFile,
/* config.basePath */ basePath, /* config.urlRoot */ urlRoot) {

return function(request, response, next) {
var requestUrl = request.url.replace(/\?.*/, '');

// redirect /__karma__ to /__karma__ (trailing slash)
if (requestUrl === urlRoot.substr(0, urlRoot.length - 1)) {
response.setHeader('Location', urlRoot);
response.writeHead(301);
return response.end('MOVED PERMANENTLY');
}

// ignore urls outside urlRoot
if (requestUrl.indexOf(urlRoot) !== 0) {
return next();
}

// remove urlRoot prefix
requestUrl = requestUrl.substr(urlRoot.length - 1);

// serve client.html
if (requestUrl === '/') {
return serveStaticFile('/client.html', response);
}

// serve karma.js
if (requestUrl === '/karma.js') {
return serveStaticFile(requestUrl, response, function(data) {
return data.replace('%KARMA_URL_ROOT%', urlRoot)
.replace('%KARMA_VERSION%', VERSION);
});
}

// serve context.html - execution context within the iframe
// or debug.html - execution context without channel to the server
if (requestUrl === '/context.html' || requestUrl === '/debug.html') {
return filesPromise.then(function(files) {
serveStaticFile(requestUrl, response, function(data) {
common.setNoCacheHeaders(response);

var scriptTags = files.included.map(function(file) {
var filePath = file.path;
var fileExt = path.extname(filePath);

if (!file.isUrl) {
// TODO(vojta): serve these files from within urlRoot as well
filePath = filePathToUrlPath(filePath, basePath);

if (requestUrl === '/context.html') {
filePath += '?' + file.mtime.getTime();
}
}

if (fileExt === '.css') {
return util.format(LINK_TAG, filePath);
}

return util.format(SCRIPT_TAG, SCRIPT_TYPE[fileExt] || 'text/javascript', filePath);
});

// TODO(vojta): don't compute if it's not in the template
var mappings = files.served.map(function(file) {
var filePath = filePathToUrlPath(file.path, basePath);

return util.format(' \'%s\': \'%d\'', filePath, file.mtime.getTime());
});

mappings = 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n';

return data.replace('%SCRIPTS%', scriptTags.join('\n')).replace('%MAPPINGS%', mappings);
});
});
}

return next();
};
};


// PUBLIC API
exports.create = createKarmaMiddleware;
7 changes: 5 additions & 2 deletions lib/proxy.js → lib/middleware/proxy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var url = require('url');
var httpProxy = require('http-proxy');

var log = require('./logger').create('proxy');
var log = require('../logger').create('proxy');


var parseProxyConfig = function(proxies) {
Expand Down Expand Up @@ -94,4 +95,6 @@ var createProxyHandler = function(proxy, proxyConfig, proxyValidateSSL) {
};


exports.createProxyHandler = createProxyHandler;
exports.create = function(/* config.proxies */ proxies, /* config.proxyValidateSSL */ validateSSL) {
return createProxyHandler(new httpProxy.RoutingProxy({changeOrigin: true}), proxies, validateSSL);
};
59 changes: 59 additions & 0 deletions lib/middleware/source-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Source Files middleware is responsible for serving all the source files under the test.
*/

var querystring = require('querystring');
var common = require('./common');
var pause = require('connect').utils.pause;


var findByPath = function(files, path) {
for (var i = 0; i < files.length; i++) {
if (files[i].path === path) {
return files[i];
}
}

return null;
};


var createSourceFilesMiddleware = function(filesPromise, serveFile,
/* config.basePath */ basePath) {

return function(request, response, next) {
var requestedFilePath = querystring.unescape(request.url.replace(/\?.*/, ''))
.replace(/^\/absolute/, '')
.replace(/^\/base/, basePath);

// Need to pause the request because of proxying, see:
// https://groups.google.com/forum/#!topic/q-continuum/xr8znxc_K5E/discussion
// TODO(vojta): remove once we don't care about Node 0.8
var pausedRequest = pause(request);

return filesPromise.then(function(files) {
// TODO(vojta): change served to be a map rather then an array
var file = findByPath(files.served, requestedFilePath);

if (file) {
serveFile(file.contentPath, response, function() {
if (/\?\d+/.test(request.url)) {
// files with timestamps - cache one year, rely on timestamps
common.setHeavyCacheHeaders(response);
} else {
// without timestamps - no cache (debug)
common.setNoCacheHeaders(response);
}
});
} else {
next();
}

pausedRequest.resume();
});
};
};


// PUBLIC API
exports.create = createSourceFilesMiddleware;
4 changes: 3 additions & 1 deletion lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,10 @@ exports.start = function(cliOptions, done) {
config: ['value', config],
preprocess: ['factory', preprocessor.createPreprocessor],
fileList: ['type', FileList],
webServer: ['factory', ws.createWebServer],
webServer: ['factory', ws.create],
// TODO(vojta): remove
customFileHandlers: ['value', []],
// TODO(vojta): remove, once karma-dart does not rely on it
customScriptTypes: ['value', []],
reporter: ['factory', reporter.createReporters],
capturedBrowsers: ['type', browser.Collection],
Expand Down
Loading

0 comments on commit 7255aa6

Please sign in to comment.