diff --git a/README.md b/README.md index 0b699e50..7fab36c6 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ This will install `http-server` globally so that it may be run from the command `-P` or `--proxy` Proxies all requests which can't be resolved locally to the given url. e.g.: -P http://someurl.com +`--push-state` Enable HTML5 push state mode where all URIs respond with index.html. (defaults to 'False') + `-S` or `--ssl` Enable https. `-C` or `--cert` Path to ssl cert file (default: cert.pem). diff --git a/bin/http-server b/bin/http-server index a238603b..df2ff5ec 100755 --- a/bin/http-server +++ b/bin/http-server @@ -31,7 +31,7 @@ if (argv.h || argv.help) { ' -U --utc Use UTC time format in log messages.', '', ' -P --proxy Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com', - '', + ' --push-state Enable HTML5 push state mode where all URIs respond with index.html. [false]', ' -S --ssl Enable https.', ' -C --cert Path to ssl cert file (default: cert.pem).', ' -K --key Path to ssl key file (default: key.pem).', @@ -101,6 +101,14 @@ function listen(port) { proxy: proxy }; + if (argv['push-state']) { + options.pushState = true; + + if (options.proxy || options.autoIndex) { + logger.info('Proxy and Auto Indexing are not supported with Push State.'.red); + } + } + if (argv.cors) { options.cors = true; } @@ -135,6 +143,10 @@ function listen(port) { logger.info('Unhandled requests will be served from: ' + proxy); } + if (options.pushState) { + logger.info('HTML5 Push State is enabled, all URLs will be changed to the index page.'); + } + logger.info('Hit CTRL-C to stop the server'); if (argv.o) { opener( diff --git a/lib/http-server.js b/lib/http-server.js index daddcaae..2e8963ef 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -47,6 +47,7 @@ function HttpServer(options) { this.showDir = options.showDir !== 'false'; this.autoIndex = options.autoIndex !== 'false'; this.contentType = options.contentType || 'application/octet-stream'; + this.pushState = options.pushState || false; if (options.ext) { this.ext = options.ext === true @@ -86,7 +87,7 @@ function HttpServer(options) { }); } - before.push(ecstatic({ + var ecstaticMiddleware = ecstatic({ root: this.root, cache: this.cache, showDir: this.showDir, @@ -94,7 +95,9 @@ function HttpServer(options) { defaultExt: this.ext, contentType: this.contentType, handleError: typeof options.proxy !== 'string' - })); + }); + + before.push(ecstaticMiddleware); if (typeof options.proxy === 'string') { var proxy = httpProxy.createProxyServer({}); @@ -106,6 +109,27 @@ function HttpServer(options) { }); } + if (options.pushState) { + // When push state is enabled, rewrite all the URLs to the index page if the status code is a 404. + before.push(function (req, res, next) { + var currentURL = req.url; + var indexPage = '/index.html'; + + // Only add paramters when they exist. + var parameterStartIndex = currentURL.indexOf('?'); + if (parameterStartIndex >= 0) { + var currentParameters = currentURL.slice(parameterStartIndex); + + req.url = indexPage + currentParameters; + } + else { + req.url = indexPage; + } + + ecstaticMiddleware(req, res, next); + }); + } + var serverOptions = { before: before, headers: this.headers, diff --git a/test/fixtures/pushStateRoot/app.js b/test/fixtures/pushStateRoot/app.js new file mode 100644 index 00000000..051f441e --- /dev/null +++ b/test/fixtures/pushStateRoot/app.js @@ -0,0 +1 @@ +// JavaScript File Exists diff --git a/test/fixtures/pushStateRoot/index.html b/test/fixtures/pushStateRoot/index.html new file mode 100644 index 00000000..9f51cbe7 --- /dev/null +++ b/test/fixtures/pushStateRoot/index.html @@ -0,0 +1 @@ +pushState Enabled. diff --git a/test/http-server-test.js b/test/http-server-test.js index a9240b28..5e5593ca 100644 --- a/test/http-server-test.js +++ b/test/http-server-test.js @@ -150,5 +150,58 @@ vows.describe('http-server').addBatch({ assert.equal(res.statusCode, 204); } } + }, + 'When push-state is enabled': { + topic: function () { + var pushStateRoot = path.join(__dirname, 'fixtures', 'pushStateRoot'); + + var server = httpServer.createServer({ + root: pushStateRoot, + pushState: true + }); + // NOTE: using 8083 because the 808[1-2] are both active and not yet shutdown. + server.listen(8083); + + this.callback(null, server); + }, + 'and a non-existant file is requested': { + topic: function () { + request('http://127.0.0.1:8083/404', this.callback); + }, + 'status code should be 200': function (res) { + assert.equal(res.statusCode, 200); + }, + 'and file content': { + topic: function (res, body) { + var self = this; + var pushStateRoot = path.join(__dirname, 'fixtures', 'pushStateRoot'); + + fs.readFile(path.join(pushStateRoot, 'index.html'), 'utf8', function (err, data) { + self.callback(err, data, body); + }); + }, + 'should match content of served file': function (err, file, body) { + assert.equal(body.trim(), file.trim()); + } + } + }, + 'and a file exists': { + topic: function () { + request('http://127.0.0.1:8083/app.js', this.callback); + }, + 'and file content': { + topic: function (res, body) { + var self = this; + var pushStateRoot = path.join(__dirname, 'fixtures', 'pushStateRoot'); + + fs.readFile(path.join(pushStateRoot, 'app.js'), 'utf8', function (err, data) { + self.callback(err, data, body); + }); + }, + 'should match content of served file': function (err, file, body) { + assert.equal(body.trim(), file.trim()); + } + } + } } }).export(module);