diff --git a/package-lock.json b/package-lock.json index 7f3cb64..9f93558 100644 --- a/package-lock.json +++ b/package-lock.json @@ -782,6 +782,17 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + } + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -1067,6 +1078,11 @@ } } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, "ejs": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.2.tgz", @@ -1077,6 +1093,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, "encoding": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", @@ -1145,6 +1166,11 @@ "is-symbol": "^1.0.2" } }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1596,6 +1622,20 @@ } } }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, "find-replace": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", @@ -3007,6 +3047,14 @@ "isobject": "^3.0.1" } }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3100,6 +3148,11 @@ "json-parse-better-errors": "^1.0.1" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -3283,11 +3336,6 @@ } } }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3857,6 +3905,11 @@ } } }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, "string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", @@ -4182,14 +4235,6 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" }, - "union": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", - "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", - "requires": { - "qs": "^6.4.0" - } - }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -4214,6 +4259,11 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -4422,6 +4472,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 0605a49..88e83f3 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "chalk": "^1.1.3", "cli-progress": "^1.4.1", "commander": "^2.12.2", + "connect": "^3.7.0", "ecstatic": "^2.2.2", "filewatcher": "^3.0.1", "fs-extra": "^4.0.0", @@ -16,7 +17,6 @@ "promptly": "^2.2.0", "qrcode": "^1.4.4", "semver": "^5.3.0", - "union": "^0.5.0", "update-notifier": "^2.2.0", "ws": "^5.2.0", "xml2js": "^0.4.17", diff --git a/src/services/GetFilesMiddleware.js b/src/services/GetFilesMiddleware.js index 0c1124f..54e7afe 100644 --- a/src/services/GetFilesMiddleware.js +++ b/src/services/GetFilesMiddleware.js @@ -22,7 +22,8 @@ module.exports = class GetFilesMiddleware extends EventEmitter { if (param !== LOCAL_FILES) { throw new Error(`Invalid parameter ${GET_FILES}=${param}`); } - return res.json(this._generateChunk(this._getLocalPath(req.url))); + res.setHeader('content-type', 'application/json'); + return res.end(JSON.stringify(this._generateChunk(this._getLocalPath(req.url)))); } next(); } diff --git a/src/services/Server.js b/src/services/Server.js index 8d300a0..ffc3cbe 100644 --- a/src/services/Server.js +++ b/src/services/Server.js @@ -3,7 +3,7 @@ const {join} = require('path'); const EventEmitter = require('events'); const {readJsonSync, existsSync, lstat} = require('fs-extra'); const ecstatic = require('ecstatic'); -const union = require('union'); +const connect = require('connect'); const portscanner = require('portscanner'); const {red, blue} = require('chalk'); const WebSocket = require('ws'); @@ -149,16 +149,11 @@ module.exports = class Server extends EventEmitter { if (!Server.externalAddresses.length) { throw new Error('No remotely accessible network interfaces'); } - this._server = union.createServer({ - before: this._createMiddlewares(appPath, main), - onError: (err, req, res) => { - this.emit('request', req, err); - res.end(); - } - }); + const app = connect(); + this._createMiddlewares(appPath, main).forEach(middleware => app.use(middleware)); let port = this._port || await this._findAvailablePort(); return new Promise((resolve, reject) => { - this._server.listen(port, err => { + this._server = app.listen(port, err => { if (err) { reject(err); } @@ -181,24 +176,36 @@ module.exports = class Server extends EventEmitter { _createMiddlewares(appPath, main) { return [ + this._createErrorHandler(), this._createRequestEmitter(), this._createGetFilesMiddleware(appPath), this._createDeliverEmitter(), this._createPackageJsonMiddleware(main), this._createBootJsMiddleware(appPath), this._createDefaultRouteMiddleware(), - ecstatic({root: appPath, showDir: false}) + ecstatic({root: appPath, showDir: false}), + this._create404Handler() ]; } _logRequest(req, err) { if (err) { - this.terminal.error(red(`${req.method} ${req.url} ${err.status}: "${err.message || err}"`)); + this.terminal.error(red(`${req.method} ${req.url}: "${err.message || err}"`)); } else { this.terminal.info(blue(`${req.method} ${req.url}`)); } } + _createErrorHandler() { + // Error handling middlewares have four parameters. + // The fourth parameter needs to be given although it's currently not used. + // eslint-disable-next-line no-unused-vars + return (err, req, res, next) => { + this.emit('request', req, err); + res.end(); + }; + } + _createRequestEmitter() { return (req, res, next) => { this.emit('request', req); @@ -228,7 +235,8 @@ module.exports = class Server extends EventEmitter { } return (req, res, next) => { if (req.url === '/package.json') { - return res.json(this._packageJson); + res.setHeader('content-type', 'application/json'); + return res.end(JSON.stringify(this._packageJson)); } next(); }; @@ -240,7 +248,8 @@ module.exports = class Server extends EventEmitter { } return (req, res, next) => { if (req.url === '/node_modules/tabris/boot.min.js') { - return res.text(getBootJs( + res.setHeader('content-type', 'text/plain'); + return res.end(getBootJs( appPath, this.debugServer.getNewSessionId(), encodeURIComponent(this.serverId) @@ -253,13 +262,22 @@ module.exports = class Server extends EventEmitter { _createDefaultRouteMiddleware() { return async (req, res, next) => { if (req.url === '/') { - res.html(await this._html.generate()); + res.setHeader('content-type', 'text/html'); + res.end(await this._html.generate()); } else { next(); } }; } + _create404Handler() { + return (req, res) => { + res.writeHead(404, {'Content-Type': 'text/plain'}); + res.end('Not found\n'); + this.emit('request', req, '404: Not found'); + }; + } + _findAvailablePort() { return portscanner.findAPortNotInUse(BASE_PORT, MAX_PORT, '127.0.0.1'); } diff --git a/test/GetFilesMiddleware.test.js b/test/GetFilesMiddleware.test.js index 73e5cb7..af177c6 100644 --- a/test/GetFilesMiddleware.test.js +++ b/test/GetFilesMiddleware.test.js @@ -41,20 +41,21 @@ describe('GetFilesMiddleware', () => { return eval(code); }, load(url) { - const json = spy(); - const next = spy(); + const end = stub(); + const setHeader = stub(); + const next = stub(); try { - middleware.handleRequest({url: url.slice(1)}, {json}, next); - if (json.notCalled && next.calledOnce) { + middleware.handleRequest({url: url.slice(1)}, {end, setHeader}, next); + if (end.notCalled && next.calledOnce) { return null; - } else if (json.calledOnce) { - return JSON.stringify(json.firstCall.args[0]); + } else if (end.calledOnce) { + return end.firstCall.args[0]; } } catch (ex) { console.error(ex.message); console.error(ex.stack); } - throw new Error(`Inconsistent handleRequest behavior ${json.callCount}/${next.callCount}`); + throw new Error(`Inconsistent handleRequest behavior ${end.callCount}/${next.callCount}`); }, createLoader: stub().returns('orgCreateLoader'), readJSON: stub().returns('orgReadJSON'), @@ -314,7 +315,7 @@ describe('GetFilesMiddleware', () => { expect(loader).to.be.null; expect(middleware.handleRequest).to.have.been.calledTwice; - expect(middleware.handleRequest.getCall(0).args[1].json).to.have.been.calledOnce; // 'req.json' + expect(middleware.handleRequest.getCall(0).args[1].end).to.have.been.calledOnce; // 'req.end' expect(middleware.handleRequest.getCall(1).args[2]).to.have.been.calledOnce; // 'next' }); @@ -323,7 +324,7 @@ describe('GetFilesMiddleware', () => { expect(loader).to.be.null; expect(middleware.handleRequest).to.have.been.calledTwice; - expect(middleware.handleRequest.getCall(0).args[1].json).to.have.been.calledOnce; // 'req.json' + expect(middleware.handleRequest.getCall(0).args[1].end).to.have.been.calledOnce; // 'req.end' expect(middleware.handleRequest.getCall(1).args[2]).to.have.been.calledOnce; // 'next' }); diff --git a/test/serve.test.js b/test/serve.test.js index d8851f0..9e24daa 100644 --- a/test/serve.test.js +++ b/test/serve.test.js @@ -253,7 +253,7 @@ describe('serve', function() { fetch(`http://127.0.0.1:${port}/non-existent`) ]); let log = stdout2.toString(); - expect(log).to.contain('GET /non-existent 404: "Not found"'); + expect(log).to.contain('GET /non-existent: "404: Not found"'); }).timeout(30000); });