From b1d321aa570ccedbd651b21b025c044f495a3712 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Wed, 10 Feb 2016 02:50:57 +0100 Subject: [PATCH 1/8] Rewrite to add Express Router support, update documentation --- .babelrc | 3 + README.md | 87 ++++++++++++++++++++++++--- index.js | 71 +--------------------- lib/index.js | 137 ++++++++++++++++++++++++++++++++++++++++++ lib/trailing-slash.js | 9 +++ package.json | 12 ++-- src/index.js | 126 ++++++++++++++++++++++++++++++++++++++ src/trailing-slash.js | 9 +++ 8 files changed, 372 insertions(+), 82 deletions(-) create mode 100644 .babelrc create mode 100644 lib/index.js create mode 100644 lib/trailing-slash.js create mode 100644 src/index.js create mode 100644 src/trailing-slash.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..22e308d --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + presets: ["es2015"] +} \ No newline at end of file diff --git a/README.md b/README.md index 60ecf98..5f45143 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,25 @@ -# express-ws # -WebSocket endpoints for express applications. Gives WebSocket connections access to functionality from express middlewares. +# express-ws -## Installation ## -`npm install express-ws` +[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) endpoints for [Express](http://expressjs.com/) applications. Lets you define WebSocket endpoints like any other type of route, and applies regular Express midddleware like for anything else. + +If you've used an older version of `express-ws` before, please make sure to read the Changelog at the bottom of this document - some things have changed. + +## Installation + +`npm install --save express-ws` ## Usage -Add this line to your express application: + +__Full documentation can be found in the API section below. This section only shows a brief example.__ + +Add this line to your Express application: + ```javascript -var expressWs = require('express-ws')(app); //app = express app +var expressWs = require('express-ws')(app); ``` Now you will be able to add WebSocket routes (almost) the same way you add other routes. The following snippet sets up a simple echo server at `/echo`. + ```javascript app.ws('/echo', function(ws, req) { ws.on('message', function(msg) { @@ -19,7 +28,22 @@ app.ws('/echo', function(ws, req) { }); ``` -## Example +It works with routers, too, this time at `/ws-stuff/echo`: + +```javascript +var router = express.Router(); + +router.ws('/echo', function(ws, req) { + ws.on('message', function(msg) { + ws.send(msg); + }); +}); + +app.use("/ws-stuff", router); +``` + +## Full example + ```javascript var express = require('express'); var app = express(); @@ -45,3 +69,52 @@ app.ws('/', function(ws, req) { app.listen(3000); ``` + +## API + +### expressWs(app, *server*, *options*) + +Sets up `express-ws` on the specified `app`. This will modify the global Router prototype for Express as well - see the `leaveRouterUntouched` option for more information on disabling this. + +* __app__: The Express application to set up `express-ws` on. +* __server__: *Optional.* When using a custom `http.Server`, you should pass it in here, so that `express-ws` can use it to set up the WebSocket upgrade handlers. If you don't specify a `server`, you will only be able to use it with the server that is created automatically when you call `app.listen`. +* __options__: *Optional.* An object containing further options. + * __leaveRouterUntouched:__ Set this to `true` to keep `express-ws` from modifying the Router prototype. You will have to manually `applyTo` every Router that you wish to make `.ws` available on, when this is enabled. + +This function will return a new `express-ws` API object, which will be referred to as `wsInstance` in the rest of the documentation. + +### wsInstance.app + +This property contains the `app` that `express-ws` was set up on. + +### wsInstance.getWss() + +Returns the underlying WebSocket server/handler. You can use `wsInstance.getWss().clients` to obtain a list of all the connected WebSocket clients for this server. + +Note that this list will include *all* clients, not just those for a specific route - this means that it's often *not* a good idea to use this for broadcasts, for example. + +### wsInstance.applyTo(router) + +Sets up `express-ws` on the given `router` (or other Router-like object). You will only need this in two scenarios: + +1. You have enabled `options.leaveRouterUntouched`, or +2. You are using a custom router that is not based on the express.Router prototype. + +In most cases, you won't need this at all. + +## Development + +This module is written in ES6, and uses Babel for compilation. What this means in practice: + +* The source code lives in the `src/` directory. +* After changing this code, make sure to run `npm run build` to compile it. + +## Changelog + +### v2.0 + +* __[BREAKING]__ Now supports Express Routers as well. A side-effect of this is that the Router prototype is modified to add a `.ws` method - in rare cases, this may interfere with existing code that relies on the Router prototype either remaining unchanged, or adding its own `.ws` method to it. + + A `leaveRouterUntouched` option has been added to prevent this behaviour; see the API documentation for more information. + +* [minor] You can now add `.ws` functionality to any custom Router object that follows the Express Router API. \ No newline at end of file diff --git a/index.js b/index.js index b33014d..0ff2253 100644 --- a/index.js +++ b/index.js @@ -1,70 +1 @@ -var url = require('url'); -var urlJoin = require('url-join'); -var http = require('http'); -var ServerResponse = http.ServerResponse; -var WebSocketServer = require('ws').Server; - -var wsServer; - -var wrapWsHandler = function(handler) { - return function(req, res, next) { - if (req.ws) { - req.wsHandled = true; - handler(req.ws, req, next); - } else { - next(); - } - }; -}; - -/** - * @param {express.Application} app - * @param {http.Server} [server] - */ -module.exports = function (app, server) { - if(!server) { - server = http.createServer(app); - - app.listen = function() - { - return server.listen.apply(server, arguments) - } - } - - wsServer = new WebSocketServer({ server: server }); - - wsServer.on('connection', function(ws) { - var response = new ServerResponse(ws.upgradeReq); - response.writeHead = function (statusCode) { - if (statusCode > 200) ws.close(); - }; - ws.upgradeReq.ws = ws; - ws.upgradeReq.url = '/.websocket/' + ws.upgradeReq.url; - - app.handle(ws.upgradeReq, response, function() { - if (!ws.upgradeReq.wsHandled) { - ws.close(); - } - }); - }); - - function addSocketRoute(route, middleware) { - var args = [].splice.call(arguments, 0); - route = '/.websocket/' + route; - - var middlewares = args.slice(1).map(wrapWsHandler); - var routeArgs = [route].concat(middlewares); - app.get.apply(app, routeArgs); - - return app; - }; - - app.ws = addSocketRoute; - - return { - app: app, - getWss: function (route) { - return wsServer; - } - }; -}; +module.exports = require("./lib"); \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..79026d7 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,137 @@ +'use strict'; + +/* This module does a lot of monkeypatching, but unfortunately that appears to be the only way to + * accomplish this kind of stuff in Express. + * + * Here be dragons. */ + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var http = require("http"); +var express = require("express"); +var ws = require("ws"); + +var trailingSlash = require("./trailing-slash"); + +/* The following fixes HenningM/express-ws#17, correctly. */ +function websocketUrl(url) { + if (url.indexOf("?") !== -1) { + var _url$split = url.split("?"); + + var _url$split2 = _slicedToArray(_url$split, 2); + + var baseUrl = _url$split2[0]; + var query = _url$split2[1]; + + + return trailingSlash(baseUrl) + ".websocket?" + query; + } else { + return trailingSlash(url) + ".websocket"; + } +} + +module.exports = function (app, server) { + var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; + + if (server == null) { + /* No HTTP server was explicitly provided, create one for our Express application. */ + server = http.createServer(app); + + app.listen = function () { + return server.listen.apply(server, arguments); + }; + } + + function wrapMiddleware(middleware) { + return function (req, res, next) { + if (req.ws != null) { + req.wsHandled = true; + /* Unpack the `.ws` property and call the actual handler. */ + middleware(req.ws, req, next); + } else { + /* This wasn't a WebSocket request, so skip this middleware. */ + next(); + } + }; + } + + function addWsMethod(target) { + if (target.ws == null) { + /* This prevents conflict with other things setting `.ws`. */ + target.ws = function (route) { + var middlewares = Array.prototype.slice.call(arguments, 1); // deopt! + var wrappedMiddlewares = middlewares.map(wrapMiddleware); + + /* We append `/.websocket` to the route path here. Why? To prevent conflicts when + * a non-WebSocket request is made to the same GET route - after all, we are only + * interested in handling WebSocket requests. + * + * Whereas the original `express-ws` prefixed this path segment, we suffix it - + * this makes it possible to let requests propagate through Routers like normal, + * which allows us to specify WebSocket routes on Routers as well \o/! */ + var wsRoute = websocketUrl(route); + + /* Here we configure our new GET route. It will never get called by a client + * directly, it's just to let our request propagate internally, so that we can + * leave the regular middleware execution and error handling to Express. */ + target.get.apply(this, [wsRoute].concat(wrappedMiddlewares)); + }; + } + } + + /* Make our custom `.ws` method available directly on the Express application. You should + * really be using Routers, though. */ + addWsMethod(app); + + /* Monkeypatch our custom `.ws` method into Express' Router prototype. This makes it possible, + * when using the standard Express Router, to use the `.ws` method without any further calls + * to `makeRouter`. When using a custom router, the use of `makeRouter` may still be necessary. + * + * This approach works, because Express does a strange mixin hack - the Router factory + * function is simultaneously the prototype that gets assigned to the resulting Router + * object. */ + if (!options.leaveRouterUntouched) { + addWsMethod(express.Router); + } + + var wsServer = new ws.Server({ server: server }); + + wsServer.on("connection", function (socket) { + var request = socket.upgradeReq; + + request.ws = socket; + request.wsHandled = false; + + /* By setting this fake `.url` on the request, we ensure that it will end up in the fake + * `.get` handler that we defined above - where the wrapper will then unpack the `.ws` + * property, indicate that the WebSocket has been handled, and call the actual handler. */ + request.url = websocketUrl(request.url); + + var dummyResponse = new http.ServerResponse(request); + + dummyResponse.writeHead = function (statusCode) { + if (statusCode > 200) { + /* Something in the middleware chain signalled an error. */ + socket.close(); + } + }; + + app.handle(request, dummyResponse, function () { + if (!request.wsHandled) { + /* There was no matching WebSocket-specific route for this request. We'll close + * the connection, as no endpoint was able to handle the request anyway... */ + socket.close(); + } + }); + }); + + return { + app: app, + getWss: function getWss() { + return wsServer; + }, + applyTo: function applyTo(router) { + addWsMethod(router); + } + }; +}; \ No newline at end of file diff --git a/lib/trailing-slash.js b/lib/trailing-slash.js new file mode 100644 index 0000000..6ed70fe --- /dev/null +++ b/lib/trailing-slash.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = function (string) { + if (string.charAt(string.length - 1) != "/") { + return string + "/"; + } else { + return string; + } +}; \ No newline at end of file diff --git a/package.json b/package.json index 102f337..e37d492 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express-ws", "version": "1.0.0-rc.2", - "description": "WebSocket endpoints for express applications", + "description": "WebSocket endpoints for Express applications", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" @@ -13,11 +13,10 @@ ], "license": "BSD-2-Clause", "dependencies": { - "url-join": "0.0.1", - "ws": "~0.4.31" + "ws": "^0.4.32" }, - "devDependencies": { - "express": "~4.0.0-rc3" + "peerDependencies": { + "express": "^4.0.0" }, "directories": { "example": "examples" @@ -34,5 +33,8 @@ "bugs": { "url": "https://github.com/HenningM/express-ws/issues" }, + "scripts": { + "build": "babel src/ -d lib/" + }, "homepage": "https://github.com/HenningM/express-ws" } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..c7bfa60 --- /dev/null +++ b/src/index.js @@ -0,0 +1,126 @@ +'use strict'; + +/* This module does a lot of monkeypatching, but unfortunately that appears to be the only way to + * accomplish this kind of stuff in Express. + * + * Here be dragons. */ + +const http = require("http"); +const express = require("express"); +const ws = require("ws"); + +const trailingSlash = require("./trailing-slash"); + +/* The following fixes HenningM/express-ws#17, correctly. */ +function websocketUrl(url) { + if (url.indexOf("?") !== -1) { + let [baseUrl, query] = url.split("?"); + + return trailingSlash(baseUrl) + ".websocket?" + query; + } else { + return trailingSlash(url) + ".websocket"; + } +} + +module.exports = function(app, server, options = {}) { + if (server == null) { + /* No HTTP server was explicitly provided, create one for our Express application. */ + server = http.createServer(app); + + app.listen = function() { + return server.listen.apply(server, arguments); + } + } + + function wrapMiddleware(middleware) { + return function(req, res, next) { + if (req.ws != null) { + req.wsHandled = true; + /* Unpack the `.ws` property and call the actual handler. */ + middleware(req.ws, req, next); + } else { + /* This wasn't a WebSocket request, so skip this middleware. */ + next(); + } + } + } + + function addWsMethod(target) { + if (target.ws == null) { /* This prevents conflict with other things setting `.ws`. */ + target.ws = function(route) { + let middlewares = Array.prototype.slice.call(arguments, 1); // deopt! + let wrappedMiddlewares = middlewares.map(wrapMiddleware) + + /* We append `/.websocket` to the route path here. Why? To prevent conflicts when + * a non-WebSocket request is made to the same GET route - after all, we are only + * interested in handling WebSocket requests. + * + * Whereas the original `express-ws` prefixed this path segment, we suffix it - + * this makes it possible to let requests propagate through Routers like normal, + * which allows us to specify WebSocket routes on Routers as well \o/! */ + let wsRoute = websocketUrl(route); + + /* Here we configure our new GET route. It will never get called by a client + * directly, it's just to let our request propagate internally, so that we can + * leave the regular middleware execution and error handling to Express. */ + target.get.apply(this, [wsRoute].concat(wrappedMiddlewares)); + } + } + } + + /* Make our custom `.ws` method available directly on the Express application. You should + * really be using Routers, though. */ + addWsMethod(app); + + /* Monkeypatch our custom `.ws` method into Express' Router prototype. This makes it possible, + * when using the standard Express Router, to use the `.ws` method without any further calls + * to `makeRouter`. When using a custom router, the use of `makeRouter` may still be necessary. + * + * This approach works, because Express does a strange mixin hack - the Router factory + * function is simultaneously the prototype that gets assigned to the resulting Router + * object. */ + if (!options.leaveRouterUntouched) { + addWsMethod(express.Router); + } + + let wsServer = new ws.Server({server: server}); + + wsServer.on("connection", function(socket) { + var request = socket.upgradeReq; + + request.ws = socket; + request.wsHandled = false; + + /* By setting this fake `.url` on the request, we ensure that it will end up in the fake + * `.get` handler that we defined above - where the wrapper will then unpack the `.ws` + * property, indicate that the WebSocket has been handled, and call the actual handler. */ + request.url = websocketUrl(request.url); + + var dummyResponse = new http.ServerResponse(request); + + dummyResponse.writeHead = function(statusCode) { + if (statusCode > 200) { + /* Something in the middleware chain signalled an error. */ + socket.close(); + } + } + + app.handle(request, dummyResponse, function() { + if (!request.wsHandled) { + /* There was no matching WebSocket-specific route for this request. We'll close + * the connection, as no endpoint was able to handle the request anyway... */ + socket.close(); + } + }) + }) + + return { + app: app, + getWss: function() { + return wsServer; + }, + applyTo: function(router) { + addWsMethod(router); + } + } +} \ No newline at end of file diff --git a/src/trailing-slash.js b/src/trailing-slash.js new file mode 100644 index 0000000..8b36042 --- /dev/null +++ b/src/trailing-slash.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = function(string) { + if (string.charAt(string.length - 1) != "/") { + return string + "/"; + } else { + return string; + } +} \ No newline at end of file From 5b8f7e762f8568d8a9c3a0b7fa1b13ae1d18ae86 Mon Sep 17 00:00:00 2001 From: Henning Morud Date: Wed, 10 Feb 2016 19:35:33 +0100 Subject: [PATCH 2/8] Run babel in prepublish task --- .gitignore | 1 + .npmignore | 1 + lib/index.js | 137 ------------------------------------------ lib/trailing-slash.js | 9 --- package.json | 12 ++-- 5 files changed, 9 insertions(+), 151 deletions(-) create mode 100644 .npmignore delete mode 100644 lib/index.js delete mode 100644 lib/trailing-slash.js diff --git a/.gitignore b/.gitignore index 72c3a8e..3d3f08d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ +lib/ *.sw[po] diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..8eba6c8 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +src/ diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 79026d7..0000000 --- a/lib/index.js +++ /dev/null @@ -1,137 +0,0 @@ -'use strict'; - -/* This module does a lot of monkeypatching, but unfortunately that appears to be the only way to - * accomplish this kind of stuff in Express. - * - * Here be dragons. */ - -var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); - -var http = require("http"); -var express = require("express"); -var ws = require("ws"); - -var trailingSlash = require("./trailing-slash"); - -/* The following fixes HenningM/express-ws#17, correctly. */ -function websocketUrl(url) { - if (url.indexOf("?") !== -1) { - var _url$split = url.split("?"); - - var _url$split2 = _slicedToArray(_url$split, 2); - - var baseUrl = _url$split2[0]; - var query = _url$split2[1]; - - - return trailingSlash(baseUrl) + ".websocket?" + query; - } else { - return trailingSlash(url) + ".websocket"; - } -} - -module.exports = function (app, server) { - var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; - - if (server == null) { - /* No HTTP server was explicitly provided, create one for our Express application. */ - server = http.createServer(app); - - app.listen = function () { - return server.listen.apply(server, arguments); - }; - } - - function wrapMiddleware(middleware) { - return function (req, res, next) { - if (req.ws != null) { - req.wsHandled = true; - /* Unpack the `.ws` property and call the actual handler. */ - middleware(req.ws, req, next); - } else { - /* This wasn't a WebSocket request, so skip this middleware. */ - next(); - } - }; - } - - function addWsMethod(target) { - if (target.ws == null) { - /* This prevents conflict with other things setting `.ws`. */ - target.ws = function (route) { - var middlewares = Array.prototype.slice.call(arguments, 1); // deopt! - var wrappedMiddlewares = middlewares.map(wrapMiddleware); - - /* We append `/.websocket` to the route path here. Why? To prevent conflicts when - * a non-WebSocket request is made to the same GET route - after all, we are only - * interested in handling WebSocket requests. - * - * Whereas the original `express-ws` prefixed this path segment, we suffix it - - * this makes it possible to let requests propagate through Routers like normal, - * which allows us to specify WebSocket routes on Routers as well \o/! */ - var wsRoute = websocketUrl(route); - - /* Here we configure our new GET route. It will never get called by a client - * directly, it's just to let our request propagate internally, so that we can - * leave the regular middleware execution and error handling to Express. */ - target.get.apply(this, [wsRoute].concat(wrappedMiddlewares)); - }; - } - } - - /* Make our custom `.ws` method available directly on the Express application. You should - * really be using Routers, though. */ - addWsMethod(app); - - /* Monkeypatch our custom `.ws` method into Express' Router prototype. This makes it possible, - * when using the standard Express Router, to use the `.ws` method without any further calls - * to `makeRouter`. When using a custom router, the use of `makeRouter` may still be necessary. - * - * This approach works, because Express does a strange mixin hack - the Router factory - * function is simultaneously the prototype that gets assigned to the resulting Router - * object. */ - if (!options.leaveRouterUntouched) { - addWsMethod(express.Router); - } - - var wsServer = new ws.Server({ server: server }); - - wsServer.on("connection", function (socket) { - var request = socket.upgradeReq; - - request.ws = socket; - request.wsHandled = false; - - /* By setting this fake `.url` on the request, we ensure that it will end up in the fake - * `.get` handler that we defined above - where the wrapper will then unpack the `.ws` - * property, indicate that the WebSocket has been handled, and call the actual handler. */ - request.url = websocketUrl(request.url); - - var dummyResponse = new http.ServerResponse(request); - - dummyResponse.writeHead = function (statusCode) { - if (statusCode > 200) { - /* Something in the middleware chain signalled an error. */ - socket.close(); - } - }; - - app.handle(request, dummyResponse, function () { - if (!request.wsHandled) { - /* There was no matching WebSocket-specific route for this request. We'll close - * the connection, as no endpoint was able to handle the request anyway... */ - socket.close(); - } - }); - }); - - return { - app: app, - getWss: function getWss() { - return wsServer; - }, - applyTo: function applyTo(router) { - addWsMethod(router); - } - }; -}; \ No newline at end of file diff --git a/lib/trailing-slash.js b/lib/trailing-slash.js deleted file mode 100644 index 6ed70fe..0000000 --- a/lib/trailing-slash.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = function (string) { - if (string.charAt(string.length - 1) != "/") { - return string + "/"; - } else { - return string; - } -}; \ No newline at end of file diff --git a/package.json b/package.json index e37d492..fe5d876 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "WebSocket endpoints for Express applications", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "prepublish": "npm run build", + "build": "babel src/ -d lib/" }, "author": "Henning Morud ", "contributors": [ @@ -33,8 +34,9 @@ "bugs": { "url": "https://github.com/HenningM/express-ws/issues" }, - "scripts": { - "build": "babel src/ -d lib/" - }, - "homepage": "https://github.com/HenningM/express-ws" + "homepage": "https://github.com/HenningM/express-ws", + "devDependencies": { + "babel-cli": "^6.5.1", + "babel-preset-es2015": "^6.5.0" + } } From b69d6c2fb7a1215cc10ff11d8e6e96967f94b5e2 Mon Sep 17 00:00:00 2001 From: Henning Morud Date: Wed, 10 Feb 2016 19:38:52 +0100 Subject: [PATCH 3/8] Add Sven Slootweg as a contributor --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index fe5d876..529d755 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "author": "Henning Morud ", "contributors": [ "Jesús Leganés Combarro ", + "Sven Slootweg ", "Andrew Phillips " ], "license": "BSD-2-Clause", From a639308eb1cf9a5882142b9f4277bfa8c6de04c4 Mon Sep 17 00:00:00 2001 From: Henning Morud Date: Wed, 10 Feb 2016 19:43:09 +0100 Subject: [PATCH 4/8] Add editorconfig file --- .editorconfig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d2a7f03 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 From 36b9891c09edbe069b42ee91c48a48797cf52a45 Mon Sep 17 00:00:00 2001 From: Henning Morud Date: Wed, 10 Feb 2016 19:55:57 +0100 Subject: [PATCH 5/8] Move changelog to a separate file --- CHANGELOG.md | 7 +++++++ README.md | 12 +----------- 2 files changed, 8 insertions(+), 11 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..be1fc49 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +### v2.0 + +* __[BREAKING]__ Now supports Express Routers as well. A side-effect of this is that the Router prototype is modified to add a `.ws` method - in rare cases, this may interfere with existing code that relies on the Router prototype either remaining unchanged, or adding its own `.ws` method to it. + + A `leaveRouterUntouched` option has been added to prevent this behaviour; see the API documentation for more information. + +* [minor] You can now add `.ws` functionality to any custom Router object that follows the Express Router API. diff --git a/README.md b/README.md index 5f45143..90a5189 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) endpoints for [Express](http://expressjs.com/) applications. Lets you define WebSocket endpoints like any other type of route, and applies regular Express midddleware like for anything else. -If you've used an older version of `express-ws` before, please make sure to read the Changelog at the bottom of this document - some things have changed. +Version 2.0 of this library contains a breaking change. Please make sure to read the CHANGELOG.md before upgrading. ## Installation @@ -108,13 +108,3 @@ This module is written in ES6, and uses Babel for compilation. What this means i * The source code lives in the `src/` directory. * After changing this code, make sure to run `npm run build` to compile it. - -## Changelog - -### v2.0 - -* __[BREAKING]__ Now supports Express Routers as well. A side-effect of this is that the Router prototype is modified to add a `.ws` method - in rare cases, this may interfere with existing code that relies on the Router prototype either remaining unchanged, or adding its own `.ws` method to it. - - A `leaveRouterUntouched` option has been added to prevent this behaviour; see the API documentation for more information. - -* [minor] You can now add `.ws` functionality to any custom Router object that follows the Express Router API. \ No newline at end of file From 05a2020f20dcc07ff99978f24ec2c2b9dec3d5b3 Mon Sep 17 00:00:00 2001 From: Henning Morud Date: Wed, 10 Feb 2016 21:53:55 +0100 Subject: [PATCH 6/8] Add lint script to package.json --- .eslintrc | 7 +++++++ package.json | 7 +++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..f644a4b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": "airbnb/base", + "rules": { + "comma-dangle": 0, + "no-param-reassign": 0 + } +} diff --git a/package.json b/package.json index 529d755..dfb044d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "prepublish": "npm run build", - "build": "babel src/ -d lib/" + "build": "babel src/ -d lib/", + "lint": "eslint src/" }, "author": "Henning Morud ", "contributors": [ @@ -38,6 +39,8 @@ "homepage": "https://github.com/HenningM/express-ws", "devDependencies": { "babel-cli": "^6.5.1", - "babel-preset-es2015": "^6.5.0" + "babel-preset-es2015": "^6.5.0", + "eslint": "^1.10.3", + "eslint-config-airbnb": "^5.0.0" } } From 257a9e8dca24c0a1fd895ebdf3fb98ce4fbcffa9 Mon Sep 17 00:00:00 2001 From: Henning Morud Date: Wed, 10 Feb 2016 22:12:13 +0100 Subject: [PATCH 7/8] Complete the transition to ES6 & airbnb JS style --- index.js | 2 +- src/index.js | 223 +++++++++++++++++++++--------------------- src/trailing-slash.js | 15 ++- 3 files changed, 116 insertions(+), 124 deletions(-) diff --git a/index.js b/index.js index 0ff2253..8a13083 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require("./lib"); \ No newline at end of file +module.exports = require("./lib").expressWs; diff --git a/src/index.js b/src/index.js index c7bfa60..3857942 100644 --- a/src/index.js +++ b/src/index.js @@ -1,126 +1,121 @@ -'use strict'; - /* This module does a lot of monkeypatching, but unfortunately that appears to be the only way to * accomplish this kind of stuff in Express. - * + * * Here be dragons. */ -const http = require("http"); -const express = require("express"); -const ws = require("ws"); +import http from 'http'; +import express from 'express'; +import ws from 'ws'; -const trailingSlash = require("./trailing-slash"); +import trailingSlash from './trailing-slash'; /* The following fixes HenningM/express-ws#17, correctly. */ function websocketUrl(url) { - if (url.indexOf("?") !== -1) { - let [baseUrl, query] = url.split("?"); - - return trailingSlash(baseUrl) + ".websocket?" + query; - } else { - return trailingSlash(url) + ".websocket"; - } + if (url.indexOf('?') !== -1) { + const [baseUrl, query] = url.split('?'); + + return `${trailingSlash(baseUrl)}.websocket?${query}`; + } + return `${trailingSlash(url)}.websocket`; } -module.exports = function(app, server, options = {}) { - if (server == null) { - /* No HTTP server was explicitly provided, create one for our Express application. */ - server = http.createServer(app); - - app.listen = function() { - return server.listen.apply(server, arguments); - } - } - - function wrapMiddleware(middleware) { - return function(req, res, next) { - if (req.ws != null) { - req.wsHandled = true; - /* Unpack the `.ws` property and call the actual handler. */ - middleware(req.ws, req, next); - } else { - /* This wasn't a WebSocket request, so skip this middleware. */ - next(); - } - } - } - - function addWsMethod(target) { - if (target.ws == null) { /* This prevents conflict with other things setting `.ws`. */ - target.ws = function(route) { - let middlewares = Array.prototype.slice.call(arguments, 1); // deopt! - let wrappedMiddlewares = middlewares.map(wrapMiddleware) +export function expressWs(app, httpServer, options = {}) { + let server = httpServer; - /* We append `/.websocket` to the route path here. Why? To prevent conflicts when - * a non-WebSocket request is made to the same GET route - after all, we are only - * interested in handling WebSocket requests. - * - * Whereas the original `express-ws` prefixed this path segment, we suffix it - - * this makes it possible to let requests propagate through Routers like normal, - * which allows us to specify WebSocket routes on Routers as well \o/! */ - let wsRoute = websocketUrl(route); + if (!server) { + /* No HTTP server was explicitly provided, create one for our Express application. */ + server = http.createServer(app); - /* Here we configure our new GET route. It will never get called by a client - * directly, it's just to let our request propagate internally, so that we can - * leave the regular middleware execution and error handling to Express. */ - target.get.apply(this, [wsRoute].concat(wrappedMiddlewares)); - } - } - } - - /* Make our custom `.ws` method available directly on the Express application. You should - * really be using Routers, though. */ - addWsMethod(app); - - /* Monkeypatch our custom `.ws` method into Express' Router prototype. This makes it possible, - * when using the standard Express Router, to use the `.ws` method without any further calls - * to `makeRouter`. When using a custom router, the use of `makeRouter` may still be necessary. - * - * This approach works, because Express does a strange mixin hack - the Router factory - * function is simultaneously the prototype that gets assigned to the resulting Router - * object. */ - if (!options.leaveRouterUntouched) { - addWsMethod(express.Router); - } - - let wsServer = new ws.Server({server: server}); - - wsServer.on("connection", function(socket) { - var request = socket.upgradeReq; - - request.ws = socket; - request.wsHandled = false; - - /* By setting this fake `.url` on the request, we ensure that it will end up in the fake - * `.get` handler that we defined above - where the wrapper will then unpack the `.ws` - * property, indicate that the WebSocket has been handled, and call the actual handler. */ - request.url = websocketUrl(request.url); - - var dummyResponse = new http.ServerResponse(request); - - dummyResponse.writeHead = function(statusCode) { - if (statusCode > 200) { - /* Something in the middleware chain signalled an error. */ - socket.close(); - } - } - - app.handle(request, dummyResponse, function() { - if (!request.wsHandled) { - /* There was no matching WebSocket-specific route for this request. We'll close - * the connection, as no endpoint was able to handle the request anyway... */ - socket.close(); - } - }) - }) - - return { - app: app, - getWss: function() { - return wsServer; - }, - applyTo: function(router) { - addWsMethod(router); - } - } -} \ No newline at end of file + app.listen = function serverListen() { + server.listen.apply(server, arguments); + }; + } + + function wrapMiddleware(middleware) { + return (req, res, next) => { + if (req.ws !== null) { + req.wsHandled = true; + /* Unpack the `.ws` property and call the actual handler. */ + middleware(req.ws, req, next); + } else { + /* This wasn't a WebSocket request, so skip this middleware. */ + next(); + } + }; + } + + function addWsMethod(target) { + if (!target.ws) { /* This prevents conflict with other things setting `.ws`. */ + target.ws = function addWsRoute(route) { + const middlewares = Array.prototype.slice.call(arguments, 1); // deopt! + const wrappedMiddlewares = middlewares.map(wrapMiddleware); + + /* We append `/.websocket` to the route path here. Why? To prevent conflicts when + * a non-WebSocket request is made to the same GET route - after all, we are only + * interested in handling WebSocket requests. + * + * Whereas the original `express-ws` prefixed this path segment, we suffix it - + * this makes it possible to let requests propagate through Routers like normal, + * which allows us to specify WebSocket routes on Routers as well \o/! */ + const wsRoute = websocketUrl(route); + + /* Here we configure our new GET route. It will never get called by a client + * directly, it's just to let our request propagate internally, so that we can + * leave the regular middleware execution and error handling to Express. */ + target.get.apply(target, [wsRoute].concat(wrappedMiddlewares)); + }; + } + } + + /* Make our custom `.ws` method available directly on the Express application. You should + * really be using Routers, though. */ + addWsMethod(app); + + /* Monkeypatch our custom `.ws` method into Express' Router prototype. This makes it possible, + * when using the standard Express Router, to use the `.ws` method without any further calls + * to `makeRouter`. When using a custom router, the use of `makeRouter` may still be necessary. + * + * This approach works, because Express does a strange mixin hack - the Router factory + * function is simultaneously the prototype that gets assigned to the resulting Router + * object. */ + if (!options.leaveRouterUntouched) { + addWsMethod(express.Router); + } + + const wsServer = new ws.Server({ server }); + + wsServer.on('connection', (socket) => { + const request = socket.upgradeReq; + + request.ws = socket; + request.wsHandled = false; + + /* By setting this fake `.url` on the request, we ensure that it will end up in the fake + * `.get` handler that we defined above - where the wrapper will then unpack the `.ws` + * property, indicate that the WebSocket has been handled, and call the actual handler. */ + request.url = websocketUrl(request.url); + + const dummyResponse = new http.ServerResponse(request); + + dummyResponse.writeHead = (statusCode) => { + if (statusCode > 200) { + /* Something in the middleware chain signalled an error. */ + socket.close(); + } + }; + + app.handle(request, dummyResponse, () => { + if (!request.wsHandled) { + /* There was no matching WebSocket-specific route for this request. We'll close + * the connection, as no endpoint was able to handle the request anyway... */ + socket.close(); + } + }); + }); + + return { + app, + getWss: () => wsServer, + applyTo: (router) => addWsMethod(router) + }; +} diff --git a/src/trailing-slash.js b/src/trailing-slash.js index 8b36042..d7bd35a 100644 --- a/src/trailing-slash.js +++ b/src/trailing-slash.js @@ -1,9 +1,6 @@ -'use strict'; - -module.exports = function(string) { - if (string.charAt(string.length - 1) != "/") { - return string + "/"; - } else { - return string; - } -} \ No newline at end of file +export default function addTrailingSlash(string) { + if (string.charAt(string.length - 1) !== '/') { + return `${string}/`; + } + return string; +} From 99456180c903b8b8848c63038d628cedef823f72 Mon Sep 17 00:00:00 2001 From: Henning Morud Date: Sat, 13 Feb 2016 11:48:29 +0100 Subject: [PATCH 8/8] Reorganized expressWs function a bit --- src/add-ws-method.js | 25 +++++++++++++++++ src/index.js | 61 +++++++----------------------------------- src/trailing-slash.js | 7 ++--- src/websocket-url.js | 11 ++++++++ src/wrap-middleware.js | 12 +++++++++ 5 files changed, 62 insertions(+), 54 deletions(-) create mode 100644 src/add-ws-method.js create mode 100644 src/websocket-url.js create mode 100644 src/wrap-middleware.js diff --git a/src/add-ws-method.js b/src/add-ws-method.js new file mode 100644 index 0000000..545925e --- /dev/null +++ b/src/add-ws-method.js @@ -0,0 +1,25 @@ +import wrapMiddleware from './wrap-middleware'; +import websocketUrl from './websocket-url'; + +export default function addWsMethod(target) { + /* This prevents conflict with other things setting `.ws`. */ + if (target.ws === null || target.ws === undefined) { + target.ws = function addWsRoute(route, ...middlewares) { + const wrappedMiddlewares = middlewares.map(wrapMiddleware); + + /* We append `/.websocket` to the route path here. Why? To prevent conflicts when + * a non-WebSocket request is made to the same GET route - after all, we are only + * interested in handling WebSocket requests. + * + * Whereas the original `express-ws` prefixed this path segment, we suffix it - + * this makes it possible to let requests propagate through Routers like normal, + * which allows us to specify WebSocket routes on Routers as well \o/! */ + const wsRoute = websocketUrl(route); + + /* Here we configure our new GET route. It will never get called by a client + * directly, it's just to let our request propagate internally, so that we can + * leave the regular middleware execution and error handling to Express. */ + this.get.apply(this, [wsRoute].concat(wrappedMiddlewares)); + }; + } +} diff --git a/src/index.js b/src/index.js index 3857942..cefc000 100644 --- a/src/index.js +++ b/src/index.js @@ -7,22 +7,13 @@ import http from 'http'; import express from 'express'; import ws from 'ws'; -import trailingSlash from './trailing-slash'; - -/* The following fixes HenningM/express-ws#17, correctly. */ -function websocketUrl(url) { - if (url.indexOf('?') !== -1) { - const [baseUrl, query] = url.split('?'); - - return `${trailingSlash(baseUrl)}.websocket?${query}`; - } - return `${trailingSlash(url)}.websocket`; -} +import websocketUrl from './websocket-url'; +import addWsMethod from './add-ws-method'; export function expressWs(app, httpServer, options = {}) { let server = httpServer; - if (!server) { + if (server === null || server === undefined) { /* No HTTP server was explicitly provided, create one for our Express application. */ server = http.createServer(app); @@ -31,42 +22,6 @@ export function expressWs(app, httpServer, options = {}) { }; } - function wrapMiddleware(middleware) { - return (req, res, next) => { - if (req.ws !== null) { - req.wsHandled = true; - /* Unpack the `.ws` property and call the actual handler. */ - middleware(req.ws, req, next); - } else { - /* This wasn't a WebSocket request, so skip this middleware. */ - next(); - } - }; - } - - function addWsMethod(target) { - if (!target.ws) { /* This prevents conflict with other things setting `.ws`. */ - target.ws = function addWsRoute(route) { - const middlewares = Array.prototype.slice.call(arguments, 1); // deopt! - const wrappedMiddlewares = middlewares.map(wrapMiddleware); - - /* We append `/.websocket` to the route path here. Why? To prevent conflicts when - * a non-WebSocket request is made to the same GET route - after all, we are only - * interested in handling WebSocket requests. - * - * Whereas the original `express-ws` prefixed this path segment, we suffix it - - * this makes it possible to let requests propagate through Routers like normal, - * which allows us to specify WebSocket routes on Routers as well \o/! */ - const wsRoute = websocketUrl(route); - - /* Here we configure our new GET route. It will never get called by a client - * directly, it's just to let our request propagate internally, so that we can - * leave the regular middleware execution and error handling to Express. */ - target.get.apply(target, [wsRoute].concat(wrappedMiddlewares)); - }; - } - } - /* Make our custom `.ws` method available directly on the Express application. You should * really be using Routers, though. */ addWsMethod(app); @@ -97,7 +52,7 @@ export function expressWs(app, httpServer, options = {}) { const dummyResponse = new http.ServerResponse(request); - dummyResponse.writeHead = (statusCode) => { + dummyResponse.writeHead = function writeHead(statusCode) { if (statusCode > 200) { /* Something in the middleware chain signalled an error. */ socket.close(); @@ -115,7 +70,11 @@ export function expressWs(app, httpServer, options = {}) { return { app, - getWss: () => wsServer, - applyTo: (router) => addWsMethod(router) + getWss: function getWss() { + return wsServer; + }, + applyTo: function applyTo(router) { + addWsMethod(router); + } }; } diff --git a/src/trailing-slash.js b/src/trailing-slash.js index d7bd35a..c7cba79 100644 --- a/src/trailing-slash.js +++ b/src/trailing-slash.js @@ -1,6 +1,7 @@ export default function addTrailingSlash(string) { - if (string.charAt(string.length - 1) !== '/') { - return `${string}/`; + let suffixed = string; + if (suffixed.charAt(suffixed.length - 1) !== '/') { + suffixed = `${suffixed}/`; } - return string; + return suffixed; } diff --git a/src/websocket-url.js b/src/websocket-url.js new file mode 100644 index 0000000..64b845f --- /dev/null +++ b/src/websocket-url.js @@ -0,0 +1,11 @@ +import trailingSlash from './trailing-slash'; + +/* The following fixes HenningM/express-ws#17, correctly. */ +export default function websocketUrl(url) { + if (url.indexOf('?') !== -1) { + const [baseUrl, query] = url.split('?'); + + return `${trailingSlash(baseUrl)}.websocket?${query}`; + } + return `${trailingSlash(url)}.websocket`; +} diff --git a/src/wrap-middleware.js b/src/wrap-middleware.js new file mode 100644 index 0000000..2c40af3 --- /dev/null +++ b/src/wrap-middleware.js @@ -0,0 +1,12 @@ +export default function wrapMiddleware(middleware) { + return (req, res, next) => { + if (req.ws !== null && req.ws !== undefined) { + req.wsHandled = true; + /* Unpack the `.ws` property and call the actual handler. */ + middleware(req.ws, req, next); + } else { + /* This wasn't a WebSocket request, so skip this middleware. */ + next(); + } + }; +}