diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3770fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +.idea +demo/public/css +demo/node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..de61590 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +.git* +demo/ \ No newline at end of file diff --git a/EUPL-1.1.txt b/EUPL-1.1.txt new file mode 100644 index 0000000..2f028a8 --- /dev/null +++ b/EUPL-1.1.txt @@ -0,0 +1,268 @@ + European Union Public Licence + V. 1.1 + EUPL © the European Community 2007 + +This European Union Public Licence (the “EUPL”) applies to the Work or Software +(as defined below) which is provided under the terms of this Licence. Any use of the +Work, other than as authorised under this Licence is prohibited (to the extent such use +is covered by a right of the copyright holder of the Work). + +The Original Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the copyright +notice for the Original Work: + +Licensed under the EUPL V.1.1 + +or has expressed by any other mean his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- The Licence: this Licence. + +- The Original Work or the Software: the software distributed and/or communicated +by the Licensor under this Licence, available as Source Code and also as Executable +Code as the case may be. + +- Derivative Works: the works or software that could be created by the Licensee, +based upon the Original Work or modifications thereof. This Licence does not define +the extent of modification or dependence on the Original Work required in order to +classify a work as a Derivative Work; this extent is determined by copyright law +applicable in the country mentioned in Article 15. + +- The Work: the Original Work and/or its Derivative Works. + +- The Source Code: the human-readable form of the Work which is the most +convenient for people to study and modify. + +- The Executable Code: any code which has generally been compiled and which is +meant to be interpreted by a computer as a program. + +- The Licensor: the natural or legal person that distributes and/or communicates the +Work under the Licence. + +- Contributor(s): any natural or legal person who modifies the Work under the +Licence, or otherwise contributes to the creation of a Derivative Work. + +- The Licensee or “You”: any natural or legal person who makes any usage of the +Software under the terms of the Licence. + +- Distribution and/or Communication: any act of selling, giving, lending, renting, +distributing, communicating, transmitting, or otherwise making available, on-line or +off-line, copies of the Work or providing access to its essential functionalities at the +disposal of any other natural or legal person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a world-wide, royalty-free, non-exclusive, sublicensable +licence to do the following, for the duration of copyright vested in the +Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Original Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display the +Work or copies thereof to the public and perform publicly, as the case may be, +the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sub-license rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to exercise his +moral right to the extent allowed by law in order to make effective the licence of the +economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non exclusive usage rights to any +patents held by the Licensor, to the extent necessary to make use of the rights granted +on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as Executable +Code. If the Work is provided as Executable Code, the Licensor provides in addition a +machine-readable copy of the Source Code of the Work along with each copy of the +Work that the Licensor distributes or indicates, in a notice following the copyright +notice attached to the Work, a repository where the Source Code is easily and freely +accessible for as long as the Licensor continues to distribute and/or communicate the +Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from any +exception or limitation to the exclusive rights of the rights owners in the Original +Work or Software, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and obligations +imposed on the Licensee. Those obligations are the following: +Attribution right: the Licensee shall keep intact all copyright, patent or trademarks +notices and all notices that refer to the Licence and to the disclaimer of warranties. + +The Licensee must include a copy of such notices and a copy of the Licence with +every copy of the Work he/she distributes and/or communicates. The Licensee must +cause any Derivative Work to carry prominent notices stating that the Work has been +modified and the date of modification. + +Copyleft clause: If the Licensee distributes and/or communicates copies of the +Original Works or Derivative Works based upon the Original Work, this Distribution +and/or Communication will be done under the terms of this Licence or of a later +version of this Licence unless the Original Work is expressly distributed only under +this version of the Licence. The Licensee (becoming Licensor) cannot offer or impose +any additional terms or conditions on the Work or Derivative Work that alter or +restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes and/or Communicates Derivative +Works or copies thereof based upon both the Original Work and another work +licensed under a Compatible Licence, this Distribution and/or Communication can be +done under the terms of this Compatible Licence. For the sake of this clause, +“Compatible Licence” refers to the licences listed in the appendix attached to this +Licence. Should the Licensee’s obligations under the Compatible Licence conflict +with his/her obligations under this Licence, the obligations of the Compatible Licence +shall prevail. + +Provision of Source Code: When distributing and/or communicating copies of the +Work, the Licensee will provide a machine-readable copy of the Source Code or +indicate a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute and/or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and reproducing +the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the power +and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings to the +Work are owned by him/her or licensed to him/her and that he/she has the power and +authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent Contributors +grant You a licence to their contributions to the Work, under the terms of this +Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +contributors. It is not a finished work and may therefore contain defects or “bugs” +inherent to this type of software development. + +For the above reason, the Work is provided under the Licence on an “as is” basis and +without warranties of any kind concerning the Work, including without limitation +merchantability, fitness for a particular purpose, absence of defects or errors, +accuracy, non-infringement of intellectual property rights other than copyright as +stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition for the +grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, material or +moral, damages of any kind, arising out of the Licence or of the use of the Work, +including without limitation, damages for loss of goodwill, work stoppage, computer +failure or malfunction, loss of data or any commercial damage, even if the Licensor +has been advised of the possibility of such damage. However, the Licensor will be +liable under statutory product liability laws as far such laws apply to the Work. + +9. Additional agreements + +While distributing the Original Work or Derivative Works, You may choose to +conclude an additional agreement to offer, and charge a fee for, acceptance of support, +warranty, indemnity, or other liability obligations and/or services consistent with this +Licence. However, in accepting such obligations, You may act only on your own +behalf and on your sole responsibility, not on behalf of the original Licensor or any +other Contributor, and only if You agree to indemnify, defend, and hold each +Contributor harmless for any liability incurred by, or claims asserted against such +Contributor by the fact You have accepted any such warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon “I agree” +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of applicable +law. Clicking on that icon indicates your clear and irrevocable acceptance of this +Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and conditions by +exercising any rights granted to You by Article 2 of this Licence, such as the use of +the Work, the creation by You of a Derivative Work or the Distribution and/or +Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution and/or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must at +least provide to the public the information requested by the applicable law regarding +the Licensor, the Licence and the way it may be accessible, concluded, stored and +reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon any +breach by the Licensee of the terms of the Licence. +Such a termination will not terminate the licences of any person who has received the +Work from the Licensee under the Licence, provided such persons remain in full +compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete agreement +between the Parties as to the Work licensed hereunder. + +If any provision of the Licence is invalid or unenforceable under applicable law, this +will not affect the validity or enforceability of the Licence as a whole. Such provision +will be construed and/or reformed so as necessary to make it valid and enforceable. +The European Commission may publish other linguistic versions and/or new versions +of this Licence, so far this is required and reasonable, without reducing the scope of +the rights granted by the Licence. New versions of the Licence will be published with +a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, have +identical value. Parties can take advantage of the linguistic version of their choice. + +14. Jurisdiction + +Any litigation resulting from the interpretation of this License, arising between the +European Commission, as a Licensor, and any Licensee, will be subject to the +jurisdiction of the Court of Justice of the European Communities, as laid down in +article 238 of the Treaty establishing the European Community. +Any litigation arising between Parties, other than the European Commission, and +resulting from the interpretation of this License, will be subject to the exclusive +jurisdiction of the competent court where the Licensor resides or conducts its primary +business. + +15. Applicable Law + +This Licence shall be governed by the law of the European Union country where the +Licensor resides or has his registered office. + +This licence shall be governed by the Belgian law if: + +- a litigation arises between the European Commission, as a Licensor, and any +Licensee; +- the Licensor, other than the European Commission, has no residence or +registered office inside a European Union country. + +=== + +Appendix + +“Compatible Licences” according to article 5 EUPL are: + +- GNU General Public License (GNU GPL) v. 2 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Common Public License v. 1.0 +- Eclipse Public License v. 1.0 +- Cecill v. 2.0 \ No newline at end of file diff --git a/README b/README deleted file mode 100644 index 8963b0a..0000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -This file was created by JetBrains WebStorm 6.0 for binding GitHub repository \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ca1d4b --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +## What's wsem? + + Web Socket Event Manager is a simple module to manage events between web socket clients. + + Its goal is to make web sockets apps clients synchronization easier. + + It is based on expressjs and socket.io. + +## Features + +TODO + +## Usage + +TODO + +Enjoy ! diff --git a/app.js b/app.js new file mode 100644 index 0000000..5fe4298 --- /dev/null +++ b/app.js @@ -0,0 +1,71 @@ +var path = require('path') + , http = require('http') + , express = require('express') + , io = require('socket.io') + , WsEventMgr = require('./lib/ws-event-mgr') + , util = require('./lib/util') + , baseDir, httpServer, ioServer, app, intervalId; + +baseDir = __dirname; + +app = express(); + +//app.set('env', 'development'); +app.set('env', 'production'); + +app.configure('all', function () { + app.set('port', process.env.PORT || 3000); + app.set('views', path.join(baseDir, '/views')); + app.engine('html', require('ejs').renderFile); + app.set('view engine', 'html'); + app.use(express.favicon()); + app.use(express.logger('dev')); + app.use(express.bodyParser()); + app.use(express.methodOverride()); + app.use(app.router); + app.use(require('less-middleware')({ + once: true, + src: path.join(baseDir, '/less'), + dest: path.join(baseDir, 'public', 'css'), + prefix: '/css', + compress: true, + debug: 'development' === app.get('env') + })); + app.use(express.static(path.join(baseDir, 'public'))); +}); + +app.configure('development', function () { + app.use(express.errorHandler()); +}); + +app.get('/ws-event-mgr.js', require('./lib/ws-event-mgr-middleware')); +app.get('/', function (req, res) { + res.render('index'); +}); + +httpServer = http.createServer(app); +httpServer.listen(app.get('port'), function () { + console.log('Express server listening on port %s', app.get('port')); +}); + +ioServer = io.listen(httpServer, { log: 'development' === app.get('env') }); + +wsem = new WsEventMgr(ioServer); +wsem.start(); + +wsem.addListener('time', function () { + if (wsem.hasClientRegistration('time')) { + if (!intervalId) { + console.log('starting time streaming'); + intervalId = setInterval(function () { + var currentTime = util.dateFormat(new Date(), '%H:%M:%S'); + console.log('sending current time'); + wsem.emit('time', currentTime); + }, 1000); + } + } else { + console.log('stopping time streaming'); + clearInterval(intervalId); + intervalId = null; + } +}); \ No newline at end of file diff --git a/demo/app.js b/demo/app.js new file mode 100644 index 0000000..3d21dce --- /dev/null +++ b/demo/app.js @@ -0,0 +1,92 @@ +var path = require('path') + , http = require('http') + , express = require('express') + , io = require('socket.io') + , WsEventMgr = require('wsem') + , util = require('./lib/util') + , baseDir, httpServer, ioServer, app, intervalId; + +baseDir = __dirname; + +// Express app creation +app = express(); + +// Web Socket Event Manager creation +wsem = new WsEventMgr(/*{ // Default values + registerEventName: 'register', // event name to use to register a wsem event + unregisterEventName: 'unregister', // event name to use to unregister a wsem event + clientScriptUrl: '/wsem.js' // wsem client side script location + }*/); + +// Express configuration + +//app.set('env', 'development'); +app.set('env', 'production'); + +app.configure('all', function () { + app.set('port', process.env.PORT || 3000); + app.set('views', path.join(baseDir, '/views')); + app.engine('html', require('ejs').renderFile); + app.set('view engine', 'html'); + app.use(express.favicon()); + app.use(express.logger('dev')); + app.use(express.bodyParser()); + app.use(express.methodOverride()); + app.use(app.router); + app.use(require('less-middleware')({ + once: true, + src: path.join(baseDir, '/less'), + dest: path.join(baseDir, 'public', 'css'), + prefix: '/css', + compress: true, + debug: 'development' === app.get('env') + })); + app.use(wsem.expressMiddleware()); // Middleware to serve the client side wsem script + app.use(express.static(path.join(baseDir, 'public'))); +}); + +app.configure('development', function () { + app.use(express.errorHandler()); +}); + +app.get('/', function (req, res) { + res.render('index'); +}); + +// Http server creation +httpServer = http.createServer(app); +// Http server start +httpServer.listen(app.get('port'), function () { + console.log('Express server listening on port %s', app.get('port')); +}); + +// Web socket server start +ioServer = io.listen(httpServer, { log: 'development' === app.get('env') }); + +// We want to be warned when a client is registering the 'time' event +wsem.addListener('time', function () { + if (wsem.hasClientRegistration('time')) { + if (!intervalId) { + console.log('starting time streaming'); + // Every second we send the time to registered clients + intervalId = setInterval(function () { + var currentTime = util.dateFormat(new Date(), '%H:%M:%S'); + console.log('sending current time'); + wsem.emit('time', currentTime); + }, 1000); + } + } else { + console.log('stopping time streaming'); + clearInterval(intervalId); + intervalId = null; + } +}); + +// Wsem start +wsem.start(ioServer, function (socket) { + // When a 'todo' is received from a client, we send it to all clients registered for that wsem event + socket.on('todo', function (todo) { + console.log('new todo :', todo); + wsem.emit('todo', todo); + }); +}); \ No newline at end of file diff --git a/demo/less/style.less b/demo/less/style.less new file mode 100644 index 0000000..3bc8d94 --- /dev/null +++ b/demo/less/style.less @@ -0,0 +1,34 @@ +html { + background-color: #123456; +} + +body { + width: 550px; + background-color: white; + margin: 0 auto; + margin-top: 20px; + padding: 10px 20px; + border-radius: 10px; + & > div { + border-top: 1px dotted black; + margin: 20px auto; + & > h3 { + color: #49f; + margin-top: 20px; + margin-bottom: 20px; + } + } +} + +h1 { + text-align: center; + color: #777; +} + +#timeContainer { + font-size: 1.4em; +} + +#timeValueContainer { + font-weight: bold; +} diff --git a/demo/lib/util.js b/demo/lib/util.js new file mode 100644 index 0000000..a7e1c1b --- /dev/null +++ b/demo/lib/util.js @@ -0,0 +1,31 @@ +var util = { + dateFormat: function (date, fstr, utc) { + utc = utc ? 'getUTC' : 'get'; + return fstr.replace(/%[YmdHMS]/g, function (m) { + switch (m) { + case '%Y': + return date[utc + 'FullYear'](); + case '%m': + m = 1 + date[utc + 'Month'](); + break; + case '%d': + m = date[utc + 'Date'](); + break; + case '%H': + m = date[utc + 'Hours'](); + break; + case '%M': + m = date[utc + 'Minutes'](); + break; + case '%S': + m = date[utc + 'Seconds'](); + break; + default: + return m.slice(1); + } + return ('0' + m).slice(-2); + }); + } +}; + +module.exports = util; \ No newline at end of file diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 0000000..196db2b --- /dev/null +++ b/demo/package.json @@ -0,0 +1,15 @@ +{ + "name": "wsem-demo-time", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "node app" + }, + "dependencies": { + "express": "3.1.0", + "ejs": "0.8.3", + "less-middleware": "0.1.10", + "socket.io": "0.9.14", + "wsem": "0.0.1" + } +} diff --git a/demo/public/js/app.js b/demo/public/js/app.js new file mode 100644 index 0000000..490e181 --- /dev/null +++ b/demo/public/js/app.js @@ -0,0 +1,42 @@ +var socket, wsem; + +// Web socket connect +socket = io.connect(window.location.origin); + +// Wsem creation +wsem = new WsEventMgr(socket); + +// We want to receive 'time' events +wsem.on('time', function (time) { + document.getElementById('timeValueContainer').innerText = time; +}); + +// When a new 'todo' is received, we update the page +function addTodo(todo) { + var todoList = document.getElementById('todoList') + , item; + item = document.createElement('li'); + item.innerText = todo; + todoList.appendChild(item); +} + +// Check toggle to start/stop listening for 'todo' event +function checkListenMode() { + if (document.getElementById('listenToggle').checked) { + wsem.on('todo', addTodo); + } else { + wsem.end('todo', addTodo); + } +} + +// First time check +checkListenMode(); +document.getElementById('listenToggle').onchange = checkListenMode; + +// When a new todo is submitted, we send it to the server +document.getElementById('newTodoForm').onsubmit = function () { + var todoField = document.getElementById('newTodoField'); + wsem.emit('todo', todoField.value); + todoField.value = ''; + return false; +}; \ No newline at end of file diff --git a/demo/views/index.html b/demo/views/index.html new file mode 100644 index 0000000..7e0ea1e --- /dev/null +++ b/demo/views/index.html @@ -0,0 +1,35 @@ + + + + + Web Socket Event Manager Demo + + + +

Web Socket Event Manager Demo

+ +
+

Shared server time

+

Current server time : ???

+
+
+

Shared todo list

+
+ +
+
+

Todo list :

+ + +
+
+ + + + + \ No newline at end of file diff --git a/lib/wsem-client.js b/lib/wsem-client.js new file mode 100644 index 0000000..d9f3f65 --- /dev/null +++ b/lib/wsem-client.js @@ -0,0 +1,18 @@ +WsEventMgr = function (socket) { + this.socket = socket; +}; + +WsEventMgr.prototype.emit = function (args) { + var argumentsArray = Array.prototype.slice.apply(arguments); + this.socket.emit.apply(this.socket, argumentsArray); +}; + +WsEventMgr.prototype.on = function(name, callback) { + this.emit('register', name); + this.socket.on(name, callback); +}; + +WsEventMgr.prototype.end = function(name, callback) { + this.emit('unregister', name); + this.socket.removeListener(name, callback); +}; diff --git a/lib/wsem.js b/lib/wsem.js new file mode 100644 index 0000000..790af6b --- /dev/null +++ b/lib/wsem.js @@ -0,0 +1,114 @@ +var path = require('path') + , WsEventMgr; + +WsEventMgr = function (options) { + this.options = options || {}; + this.options.registerEventName = this.options.registerEventName || 'register'; + this.options.unregisterEventName = this.options.unregisterEventName || 'unregister'; + this.options.clientScriptUrl = this.options.clientScriptUrl || '/wsem.js'; + this.sockets = {}; + this.clientRegistrations = []; + this.registrationListeners = []; +}; + +WsEventMgr.prototype.start = function (server, connectCallback, disconnectCallback) { + var that = this; + that.server = server; + that.server.on('connection', function (socket) { + that.sockets[socket.id] = socket; + socket.on(that.options.registerEventName, function (event) { + if (!that.clientRegistrations[event]) { + that.clientRegistrations[event] = []; + } + that.clientRegistrations[event].push(socket.id); + if (that.registrationListeners[event]) { + that.registrationListeners[event].forEach(function (listener) { + listener(that.clientRegistrations[event]); + }); + } + }); + socket.on(that.options.unregisterEventName, function (event) { + var index; + index = that.clientRegistrations[event].indexOf(socket.id); + if (index !== -1) { + that.clientRegistrations[event].splice(index, 1); + } + if (that.registrationListeners[event]) { + that.registrationListeners[event].forEach(function (listener) { + listener(that.clientRegistrations[event]); + }); + } + }); + socket.on('disconnect', function () { + var index, event; + for (event in that.clientRegistrations) { + index = that.clientRegistrations[event].indexOf(socket.id); + if (index !== -1) { + that.clientRegistrations[event].splice(index, 1); + if (that.registrationListeners[event]) { + that.registrationListeners[event].forEach(function (listener) { + listener(that.clientRegistrations[event]); + }); + } + } + } + delete that.sockets[socket.id]; + disconnectCallback && disconnectCallback(); + }); + connectCallback && connectCallback(socket); + }); +}; + +WsEventMgr.prototype.addListener = function (event, listener) { + if (!this.registrationListeners[event]) { + this.registrationListeners[event] = []; + } + this.registrationListeners[event].push(listener); +}; + +WsEventMgr.prototype.removeListener = function (event, listener) { + var index; + if (!this.registrationListeners[event]) { + return false; + } + index = this.registrationListeners[event].indexOf(listener); + if (index == -1) { + return false; + } + this.registrationListeners[event].splice(index); + return true; +}; + +WsEventMgr.prototype.on = function (name, callback) { + this.server.on(name, callback); +}; + +WsEventMgr.prototype.emit = function (name, data) { + var i, registeredSockets, socket; + registeredSockets = this.clientRegistrations[name]; + if (!registeredSockets) return; + for (i = 0; i < registeredSockets.length; i++) { + socket = this.sockets[registeredSockets[i]]; + if (socket) { + socket.emit(name, data); + } + } +}; + +WsEventMgr.prototype.hasClientRegistration = function (name) { + return this.clientRegistrations[name] && this.clientRegistrations[name].length; +}; + +WsEventMgr.prototype.expressMiddleware = function () { + var that = this; + return function (req, res, next) { + if ('GET' === req.method && that.options.clientScriptUrl === req.url) { + res.charset = 'utf-8'; + res.sendfile(path.join(__dirname, 'wsem-client.js')); + } else { + next(); + } + } +}; + +module.exports = WsEventMgr; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..18c3383 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "wsem", + "version": "0.0.1", + "description":"Simple module to manage events between web socket clients", + "homepage":"https://github.com/openhoat/wsem", + "main": "lib/wsem.js", + "author":{ + "name":"Olivier Penhoat", + "email":"openhoat@gmail.com" + }, + "repository":{ + "type":"git", + "url":"git://github.com/openhoat/wsem.git" + }, + "bugs":{ + "url":"https://github.com/openhoat/wsem/issues" + }, + "licenses":[ + { + "type":"EUPL", + "url":"https://github.com/openhoat/wsem/blob/master/EUPL-1.1.txt" + } + ], + "engines":{ + "node":"*" + } +}