diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cdfdee7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: + - node +cache: + directories: + - node_modules +before_install: + - sudo apt-get -qq update + - sudo apt-get install -y build-essential libavahi-compat-libdnssd-dev +before_script: + - npm install -g gulp-cli +script: gulp +script: npm test \ No newline at end of file diff --git a/README.md b/README.md index a77e4b2..191665c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ MultiCast v1.0 ========= [![npm version](https://badge.fury.io/js/multicast.svg)](https://badge.fury.io/js/multicast) +[![Build Status](https://travis-ci.org/superhawk610/multicast.svg?branch=wip)](https://travis-ci.org/superhawk610/multicast) :green_heart: A persistent solution to presenting content across multiple Chromecast devices, inspired by [Greenscreen](http://greenscreen.io/). diff --git a/app/config.js b/app/config.js index a685d1e..ba66013 100644 --- a/app/config.js +++ b/app/config.js @@ -64,7 +64,8 @@ var startConfig = () => { console.log('') console.log('Configuration updated! To get started, just run\n' + '\n' + - ' multicast start') + ' multicast start' + '\n') }) rl.close() } else { diff --git a/app/lib/channels.js b/app/lib/channels.js new file mode 100644 index 0000000..b9e83cf --- /dev/null +++ b/app/lib/channels.js @@ -0,0 +1,44 @@ +'use strict' + +const Channel = require('../models/Channel') + +const devices = require('./devices') + +let channels = [] + +const func = { + init: () => func.refresh(), + refresh: () => { + Channel.find() + .sort('name') + .exec((err, _channels) => { + if (err) console.log(err) + channels = _channels + }) + }, + list: () => channels, + withId: id => channels.find(c => c._id == id), + create: (opts, callback) => { + var c = new Channel(opts) + c.save((err, channel) => { + if (err) console.log(err) + channels.push(c) + callback(channel._id) + }) + }, + update: (id, opts, callback) => { + Channel.update({ _id: id }, opts, err => { + Object.assign(func.withId(id), opts) + opts._id = id + devices.updateChannel(opts, () => callback(id)) + }) + }, + remove: (id, callback) => { + Channel.remove({ _id: id }, () => { + channels.splice(channels.findIndex(c => c._id == id), 1) // remove from local listing + devices.removeChannel(id, () => callback()) + }) + } +} + +module.exports = func \ No newline at end of file diff --git a/app/lib/config.js b/app/lib/config.js index 436b51d..d525ef4 100644 --- a/app/lib/config.js +++ b/app/lib/config.js @@ -1,3 +1,5 @@ +'use strict' + const fs = require('fs') const path = require('path') @@ -11,20 +13,25 @@ const configVariables = [ 'mongoPort' ] -try { +const port = 3944 +try { // Attempts to pull entire config from environment variables. If any key is // not found, the configuration will instead be drawn from the .config file // which should be placed in the project root. var envUsed = true configVariables.forEach(envVar => { - if (process.env[envVar] != undefined) module.exports[envVar] = process.env[envVar] + if (process.env[envVar] != undefined) + module.exports[envVar] = process.env[envVar] else envUsed = false // If any are empty, the .config file will be used }) // Skips if the full config is already in the environment variables - if (!envUsed) module.exports = JSON.parse(fs.readFileSync(configPath)) - + if (!envUsed) { + let opts = JSON.parse(fs.readFileSync(configPath)) + opts.port = port + module.exports = opts + } } catch (e) { console.log(`No config file found in ${configPath} Please run diff --git a/app/lib/connection.js b/app/lib/connection.js new file mode 100644 index 0000000..d19922f --- /dev/null +++ b/app/lib/connection.js @@ -0,0 +1,114 @@ +'use strict' + +const Client = require('castv2').Client +const config = require('./config') + +let func = { + establish: (d, disconnectCallback) => { + let host = d.address + d.connectionFailCount = 0 + if (d.status == 'offline' || d.status == 'waiting') { + const client = new Client() + client.connect(host, () => { + // reset number of failed connection attempts + d.connectionFailCount = 0 + + // create various namespace handlers + d.connection = client.createChannel( + 'sender-0', + 'receiver-0', + 'urn:x-cast:com.google.cast.tp.connection', + 'JSON' + ) + d.heartbeat = client.createChannel( + 'sender-0', + 'receiver-0', + 'urn:x-cast:com.google.cast.tp.heartbeat', + 'JSON' + ) + d.receiver = client.createChannel( + 'sender-0', + 'receiver-0', + 'urn:x-cast:com.google.cast.receiver', + 'JSON' + ) + + // establish virtual connection to the receiver + d.connection.send({ type: 'CONNECT' }) + + // start heartbeating + d.missedHeartbeats = 0 + d.pulse = setInterval(() => { + d.missedHeartbeats++ + if (d.missedHeartbeats > 6) { + // receiver has been offline for more than 30 seconds + d.status = 'offline' // mark receiver as offline + clearInterval(d.pulse) // stop checking for pulse :( + func.addError( + d, + 'Receiver missed too many heartbeats, it is likely offline.' + ) + } else d.heartbeat.send({ type: 'PING' }) + }, 5 * 1000) + d.heartbeat.on('message', (data, broadcast) => { + if (data.type == 'PONG') { + d.missedHeartbeats = 0 + d.status = 'online' + func.clearErrors(d) + } + }) + + // launch hub app + d.receiver.send({ type: 'LAUNCH', appId: config.appId, requestId: 1 }) + + // monitor receiver status updates to insure hub is open + d.receiver.on('message', (data, broadcast) => { + if (data.type != 'RECEIVER_STATUS') + func.addError(d, `Message from receiver: ${data.type}`) + if ((data.type = 'RECEIVER_STATUS')) { + // data.status contains relevant information about current app, volume, etc + if (data.status && data.status.applications) { + var apps = data.status.applications + + /* Backdrop means that our hub applications has stopped running, so we need to restart it */ + if (apps.find(a => a.displayName == 'Backdrop')) { + func.addError(d, 'Receiver closed hub, relaunching...') + d.receiver.send({ + type: 'LAUNCH', + appId: config.appId, + requestId: 1 + }) + } + } + } + }) + }) + client.on('error', () => { + d.connectionFailCount++ + if (d.connectionFailCount > 6) { + // receiver hasn't responded after 60 seconds + clearTimeout(d.connectionFail) + func.addError( + d, + 'Receiver is unresponsive, attempting to reconnect...' + ) + } else d.connectionFail = setTimeout(disconnectCallback, 10 * 1000) + }) + } + }, + hasErrors: () => Object.keys(errors).length > 0, + getErrors: () => errors, + addError: (device, message) => { + if (!errors[device.deviceId]) + errors[device.deviceId] = { + name: device.location, + messages: [] + } + if (errors[device.deviceId].messages.indexOf(message) == -1) + errors[device.deviceId].messages.push(message) + }, + clearErrors: device => delete errors[device.deviceId] + }, + errors = {} + +module.exports = func diff --git a/app/lib/dbConnect.js b/app/lib/dbConnect.js index 91832fa..5cda28f 100644 --- a/app/lib/dbConnect.js +++ b/app/lib/dbConnect.js @@ -1,9 +1,19 @@ +'use strict' + const mongoose = require('mongoose') module.exports = config => { - const userPassword = config.mongoUser ? `${config.mongoUser}:${config.mongoPass}@` : '' - mongoose.connect( - `mongodb://${userPassword}${config.mongoHost}:${config.mongoPort}/multicast?authSource=${config.mongoAuthSource}`, { - useMongoClient: true - }).on('error', () => console.log('Could not connect to Mongo.')) -} \ No newline at end of file + const userPassword = config.mongoUser + ? `${config.mongoUser}:${config.mongoPass}@` + : '' + mongoose + .connect( + `mongodb://${userPassword}${config.mongoHost}:${ + config.mongoPort + }/multicast?authSource=${config.mongoAuthSource}`, + { + useMongoClient: true + } + ) + .on('error', () => console.log('Could not connect to Mongo.')) +} diff --git a/app/lib/devices.js b/app/lib/devices.js new file mode 100644 index 0000000..10a4127 --- /dev/null +++ b/app/lib/devices.js @@ -0,0 +1,148 @@ +'use strict' + +const mdns = require('mdns') + +const Chromecast = require('../models/Chromecast') +const Channel = require('../models/Channel') + +const sockets = require('./sockets') +const connection = require('./connection') + +let devices = [] + +const func = { + init: () => { + Chromecast.find() + .populate('channel') + .exec((err, chromecasts) => { + devices = chromecasts + devices.forEach(d => (d.status = 'offline')) + findDevices() + /* start interval to continue polling for device status */ + setInterval(() => { + func.refresh() + }, 30 * 1000) + }) + }, + refresh: () => findDevices(), + list: () => devices, + isRegistered: id => devices.findIndex(d => d.deviceId == id) > -1, + isOffline: id => func.withId(id).status == 'offline', + isOnline: id => func.withId(id).status == 'online', + setStatus: (id, status) => (func.withId(id).status = status), + withId: id => devices.find(d => d.deviceId == id), + withHost: host => devices.find(d => d.address == host), + update: (id, opts) => { + let i = devices.findIndex(d => d.deviceId == id) + Object.assign(devices[i], opts) + }, + register: (id, opts) => { + let d = func.withId(id) + + delete d.unregistered // mark local info as registered + func.update(id, opts) // update local info with any new details + + /* Launch hub on newly registered device */ + let c = sockets.withHost(d.address) + if (c) + c.emit('register', d.deviceId) // emit 'register' to have setup page redirected + else launchHub(d.address) // establish connection if client hasn't already connected + }, + reconnect: host => launchHub(host), + updateChannel: (channel, callback) => { + for (let i in devices) { + let d = devices[i] + if (d.channel && d.channel._id == channel._id) { + Object.assign(d.channel, channel) + let c = sockets.withHost(d.address) + if (c) c.emit('refresh') + } + } + callback() + }, + removeChannel: (id, callback) => { + for (let i in devices) { + let d = devices[i] + if (d.channel && d.channel._id == id) delete d.channel + let c = sockets.withHost(d.address) + if (c) c.emit('change_channel', null) + } + + Chromecast.update({ channel: id }, { $unset: { channel: 1 } }, err => { + if (err) console.log(err) + callback() + }) + }, + refreshChannelPath: _path => { + for (let i in devices) { + let d = devices[i] + if (d.channel && d.channel.URLs[0].match(new RegExp(`/${_path}`))) { + let c = sockets.withHost(d.address) + if (c) c.emit('refresh') + } + } + }, + refreshAll: callback => { + for (let i in devices) { + let d = devices[i], + c = sockets.withHost(d.address) + if (c) c.emit('refresh') + } + callback() + } +} + +const findDevices = () => { + /* Look for mDNS Cast devices on local network */ + let browser = mdns.createBrowser(mdns.tcp('googlecast')) + + /* Only scan IPv4 addresses */ + mdns.Browser.defaultResolverSequence[1] = + 'DNSServiceGetAddrInfo' in mdns.dns_sd + ? mdns.rst.DNSServiceGetAddrInfo() + : mdns.rst.getaddrinfo({ families: [4] }) + + browser.on('serviceUp', service => { + // service.name: Chromecast-hexadecimalid + let _id = service.name.split('-'), + id = _id.pop() + + // fix for groups with IDs that resemble Google-Cast-Group-{deviceId}-1 + while (_id.length > 0 && id.length < 3) id = _id.pop() + + /* If device is registered, append local statistics */ + if (func.isRegistered(id)) { + func.update(id, { + name: service.txtRecord.fn, + address: service.addresses[0], + port: service.port + }) + if (func.isOffline(id)) { + func.setStatus(id, 'waiting') + launchHub(service.addresses[0]) + } + + /* Otherwise, add info for unregistered device */ + } else { + devices.push({ + unregistered: true, + deviceId: id, + name: service.txtRecord.fn, + address: service.addresses[0], + port: service.port + }) + } + }) + + /* Begin searching */ + browser.start() + + /* Stop searching after 15 seconds */ + setTimeout(() => browser.stop(), 15 * 1000) + }, + /* Establish connection with Chromecast */ + launchHub = host => { + if (host) connection.establish(func.withHost(host), () => launchHub(host)) + } + +module.exports = func diff --git a/app/lib/sockets.js b/app/lib/sockets.js new file mode 100644 index 0000000..7e99973 --- /dev/null +++ b/app/lib/sockets.js @@ -0,0 +1,26 @@ +'use strict' + +const stripIPv6 = require('./stripIPv6') +let io + +let clients = [] + +const func = { + init: server => { + io = require('socket.io')(server) + io.on('connection', client => { + clients.push(client) + client.on('disconnect', () => { + console.log( + 'disconnected client with host:', + stripIPv6(client.handshake.address) + ) + clients.splice(clients.indexOf(client), 1) + }) + }) + }, + list: () => clients, + withHost: host => clients.find(c => stripIPv6(c.handshake.address) == host) +} + +module.exports = func diff --git a/app/lib/stripIPv6.js b/app/lib/stripIPv6.js new file mode 100644 index 0000000..715caaa --- /dev/null +++ b/app/lib/stripIPv6.js @@ -0,0 +1 @@ +module.exports = ip => ip.replace(/^.*:/, '') diff --git a/app/lib/takeover.js b/app/lib/takeover.js new file mode 100644 index 0000000..4c47014 --- /dev/null +++ b/app/lib/takeover.js @@ -0,0 +1,27 @@ +'use strict' + +const Channel = require('../models/Channel') +const devices = require('../lib/devices') +const sockets = require('../lib/sockets') + +var takeover = null + +const func = { + isActive: () => takeover != null, + channel: () => takeover, + activate: (channelId, callback) => { + Channel.findOne({ _id: channelId }, (err, channel) => { + if (err) console.log(err) + + takeover = channel + devices.refreshAll(() => callback()) + }) + }, + deactivate: callback => { + takeover = null + sockets.list().forEach(c => c.emit('refresh')) + callback() + } +} + +module.exports = func \ No newline at end of file diff --git a/app/lib/ux.js b/app/lib/ux.js new file mode 100644 index 0000000..35a89b0 --- /dev/null +++ b/app/lib/ux.js @@ -0,0 +1,19 @@ +'use strict' + +const errors = [ + 'Houston, we have a problem.', + 'Yikes!', + "Something doesn't look right...", + "Something's fishy." + ], + atRandom = arr => { + let min = 0, + max = arr.length - 1 + return arr[Math.floor(Math.random() * (max - min + 1)) + min] + } + +let func = { + error: () => atRandom(errors) +} + +module.exports = func diff --git a/app/main.js b/app/main.js index 24f5dce..f4df050 100644 --- a/app/main.js +++ b/app/main.js @@ -4,9 +4,6 @@ const express = require('express') const app = express() const server = require('http').createServer(app) -const io = require('socket.io')(server) -const Client = require('castv2').Client -const mdns = require('mdns') const mongoose = require('mongoose') mongoose.Promise = require('bluebird') const bodyParser = require('body-parser') @@ -14,223 +11,58 @@ const path = require('path') const fs = require('fs') const Chromecast = require('./models/Chromecast') -const Channel = require('./models/Channel') const config = require('./lib/config') const dbConnect = require('./lib/dbConnect') +const stripIPv6 = require('./lib/stripIPv6') -const port = 3944 +const devices = require('./lib/devices') +const channels = require('./lib/channels') +const sockets = require('./lib/sockets') +const takeover = require('./lib/takeover') +const connection = require('./lib/connection') +const ux = require('./lib/ux') + +const port = config.port const serveOnly = process.argv.find(arg => arg == '--serve-only') // prettier-ignore-block -var takeover = null - +/* Establish database connection */ dbConnect(config) -/* Establish connection with Chromecast devices on local network */ - -var devices = [], - findDevices = () => { - /* Look for mDNS Cast devices on local network */ - var browser = mdns.createBrowser(mdns.tcp('googlecast')) - - /* Only scan IPv4 addresses */ - mdns.Browser.defaultResolverSequence[1] = - 'DNSServiceGetAddrInfo' in mdns.dns_sd - ? mdns.rst.DNSServiceGetAddrInfo() - : mdns.rst.getaddrinfo({ families: [4] }) - - browser.on('serviceUp', service => { - // service.name: Chromecast-hexadecimalid - var id = service.name.split('-').pop(), - i = devices.findIndex(d => d.deviceId == id) - - /* If device is registered, append local statistics */ - if (i > -1) { - devices[i].name = service.txtRecord.fn - devices[i].address = service.addresses[0] - devices[i].port = service.port - if (devices[i].status == 'offline') { - devices[i].status = 'waiting' - launchHub(service.addresses[0]) - } - - /* Otherwise, add info for unregistered device */ - } else { - devices.push({ - unregistered: true, - deviceId: id, - name: service.txtRecord.fn, - address: service.addresses[0], - port: service.port - }) - } - }) - - /* Begin searching */ - browser.start() - - /* Stop searching after 15 seconds */ - setTimeout(() => browser.stop(), 15 * 1000) - }, - /* Establish connection with Chromecast */ - launchHub = host => { - if (host) { - var d = devices.find(d => d.address == host) - - d.connectionFailCount = 0 - if (d.status == 'offline' || d.status == 'waiting') { - const client = new Client() - client.connect(host, () => { - // reset number of failed connection attempts - d.connectionFailCount = 0 - - // create various namespace handlers - d.connection = client.createChannel( - 'sender-0', - 'receiver-0', - 'urn:x-cast:com.google.cast.tp.connection', - 'JSON' - ) - d.heartbeat = client.createChannel( - 'sender-0', - 'receiver-0', - 'urn:x-cast:com.google.cast.tp.heartbeat', - 'JSON' - ) - d.receiver = client.createChannel( - 'sender-0', - 'receiver-0', - 'urn:x-cast:com.google.cast.receiver', - 'JSON' - ) - - // establish virtual connection to the receiver - d.connection.send({ type: 'CONNECT' }) - - // start heartbeating - d.missedHeartbeats = 0 - d.pulse = setInterval(() => { - d.missedHeartbeats++ - if (d.missedHeartbeats > 6) { - // receiver has been offline for more than 30 seconds - d.status = 'offline' // mark receiver as offline - clearInterval(d.pulse) // stop checking for pulse :( - } else d.heartbeat.send({ type: 'PING' }) - }, 5 * 1000) - d.heartbeat.on('message', (data, broadcast) => { - if (data.type == 'PONG') { - d.missedHeartbeats = 0 - d.status = 'online' - } - }) - - // launch hub app - d.receiver.send({ type: 'LAUNCH', appId: config.appId, requestId: 1 }) - - // monitor receiver status updates to insure hub is open - d.receiver.on('message', (data, broadcast) => { - if (data.type != 'RECEIVER_STATUS') console.log(data.type) - if ((data.type = 'RECEIVER_STATUS')) { - // data.status contains relevant information about current app, volume, etc - if (data.status && data.status.applications) { - var apps = data.status.applications - - /* Backdrop means that our hub applications has stopped running, so we need to restart it */ - if (apps.find(a => a.displayName == 'Backdrop')) { - console.log('relaunching hub...') - d.receiver.send({ - type: 'LAUNCH', - appId: config.appId, - requestId: 1 - }) - } - } - } - }) - }) - client.on('error', () => { - d.connectionFailCount++ - if (d.connectionFailCount > 6) { - // receiver hasn't responded after 60 seconds - clearTimeout(d.connectionFail) - } else d.connectionFail = setTimeout(() => launchHub(host), 10 * 1000) - }) - } - } - } +/* Load initial channel listing */ +channels.init() /* Establish socket.io service */ - -var clients = [] -io.on('connection', client => { - clients.push(client) - client.on('disconnect', () => { - var i = clients.indexOf(client) - clients.splice(i, 1) - }) -}) +sockets.init(server) /* Express Setup */ - -app.set('views', path.join(__dirname, 'views')) app.set('view engine', 'pug') - -app.use(express.static(path.resolve(__dirname, '..', 'public'))) +app.set('views', path.join(__dirname, 'views')) app.use(bodyParser.urlencoded({ extended: false })) +app.use(express.static(path.resolve(__dirname, '..', 'public'))) +app.locals.connection = connection +app.locals.ux = ux /* Home Page */ - app.get('/', (req, res) => { res.render('index', { render: 'home', takeover: takeover }) }) /* Nyan!! */ - app.get('/nyan', (req, res) => { res.render('nyan', {}) }) /* Basic Message Channel */ - -app.get('/message', (req, res) => { - fs.readFile(path.resolve(__dirname, '..', 'message.txt'), (err, data) => { - if (err || data == '') - res.render('message', { message: 'No message configured.' }) - else res.render('message', { message: data }) - }) -}) - -app.get('/message/edit', (req, res) => { - fs.readFile(path.resolve(__dirname, '..', 'message.txt'), (err, data) => { - if (err) res.render('message-edit', { message: '' }) - else res.render('message-edit', { message: data }) - }) -}) - -app.post('/message/edit', (req, res) => { - fs.writeFile( - path.resolve(__dirname, '..', 'message.txt'), - req.body.message, - err => console.log(err) - ) - for (var i in devices) { - var d = devices[i] - if (d.channel && d.channel.URLs[0].match(new RegExp(`${port}/message`))) { - var c = clients.find(c => stripIPv6(c.handshake.address) == d.address) - if (c) c.emit('refresh') - } - } - res.sendStatus(200) -}) +app.use('/message', require('./routes/message')) /* Landing Page */ - app.get('/landing', (req, res) => { var ip = stripIPv6(req.connection.remoteAddress), // Get IPv4 address of device - d = devices.find(d => d.address == ip) // Find local info for device + d = devices.withHost(ip) // Find local info for device if (d) - res.redirect(`/device/${d.deviceId}`) // Redirect to device display // If not recognized, display information + res.redirect(`/devices/${d.deviceId}`) // Redirect to device display // Display device info else res.render('setup-chromecast', { device: { @@ -244,352 +76,24 @@ app.get('/landing', (req, res) => { }) /* Devices */ - -app.get('/devices', (req, res) => { - res.render('index', { render: 'devices', devices: devices }) -}) - -app.get('/device/new', (req, res) => { - Channel.find() - .sort('name') - .exec((err, channels) => { - if (err) console.log(err) - res.render('index', { - render: 'device', - devices: devices.filter(d => d.unregistered), - channels: channels - }) - }) -}) - -app.post('/device/new', (req, res) => { - var c = new Chromecast(req.body) - c.save((err, device) => { - if (err) console.log(err) - - /* Mark local info as registered */ - var i = devices.findIndex(d => d.deviceId == req.body.deviceId) - delete devices[i].unregistered - - /* Update local info with any new details */ - devices[i] = Object.assign(devices[i], req.body) - - /* Launch hub on newly registered device */ - var c = clients.find( - c => stripIPv6(c.handshake.address) == devices[i].address - ) - if (c) - c.emit('register', devices[i].deviceId) // emit 'register' to have setup page redirected - else launchHub(devices[i].address) // establish connection if client hasn't already connected - - res.send(device.deviceId) - }) -}) - -app.get('/device/:device_id', (req, res) => { - Chromecast.findOne({ deviceId: req.params.device_id }) - .populate('channel') - .exec((err, device) => { - if (err) console.log(err) - if (device && device.channel) { - if (takeover) - res.render(`layouts/${takeover.layout}`, { - deviceId: req.params.device_id, - channel: takeover, - casting: true - }) - else { - /* device registered and channel set - display device page */ - res.render(`layouts/${device.channel.layout}`, { - deviceId: req.params.device_id, - channel: device.channel, - casting: true - }) - } - } else { - var localDevice = devices.find(d => d.deviceId == req.params.device_id) - if (device) { - if (takeover) - res.render(`layouts/${takeover.layout}`, { - deviceId: req.params.device_id, - channel: takeover, - casting: true - }) - else { - /* device registered but no channel set - display setup page */ - res.render('setup-chromecast', { - device: Object.assign(localDevice, device), - registered: true, - setupUrl: `${req.protocol}://${req.hostname}:${port}/` - }) - } - } else { - /* device is not registered - display setup page */ - res.render('setup-chromecast', { - device: Object.assign(localDevice, device), - registered: false, - setupUrl: `${req.protocol}://${req.hostname}:${port}/` - }) - } - } - }) -}) - -app.get('/device/:device_id/connect', (req, res) => { - var d = devices.find(d => d.deviceId == req.params.device_id) - if (d.status == 'offline' || d.status == 'waiting') launchHub(d.address) - else { - // launch hub if not already open - // hard reload page if already open - var c = clients.find(c => stripIPv6(c.handshake.address) == d.address) - if (c) c.emit('refresh') - } - res.sendStatus(200) -}) - -app.post('/device/:device_id/edit', (req, res) => { - Chromecast.update( - { deviceId: req.params.device_id }, - req.body, - (err, numAffected, response) => { - if (err) console.log(err) - var i = devices.findIndex(d => d.deviceId == req.params.device_id) - devices[i].location = req.body.location // update local info with location - if (req.body.channel) { - Channel.findOne({ _id: req.body.channel }).exec((err, channel) => { - var c = clients.find( - c => stripIPv6(c.handshake.address) == devices[i].address - ) - if (c) c.emit('change_channel', channel) - devices[i].channel = channel // update local info with new channel info for any applicable devices - thing({ obj: { obj: 'foo' } }, bar) - res.send(req.params.device_id) - }) - } else { - var c = clients.find( - c => stripIPv6(c.handshake.address) == devices[i].address - ) - if (c) c.emit('change_channel', null) - res.send(req.params.device_id) - } - } - ) -}) - -app.delete('/device/:device_id/edit', (req, res) => { - Chromecast.remove({ deviceId: req.params.device_id }, () => { - /* Mark local info for device as unregistered */ - var i = devices.findIndex(d => d.deviceId == req.params.device_id) - devices[i].unregistered = true - delete devices[i].channel - delete devices[i].location - res.sendStatus(200) - }) -}) - -app.get('/device/:device_id/edit', (req, res) => { - Chromecast.findOne({ deviceId: req.params.device_id }) - .populate('channel') - .exec((err, device) => { - if (err) console.log(err) - Channel.find() - .sort('name') - .exec((err, channels) => { - if (err) console.log(err) - if (device) { - var localDevice = devices.find( - d => d.deviceId == req.params.device_id - ) - res.render('index', { - render: 'device', - device: Object.assign(localDevice, device), - channels: channels - }) - } else res.render('index', {}) - }) - }) -}) +app.use('/devices', require('./routes/devices')) /* Channels */ +app.use('/channels', require('./routes/channels')) -app.get('/channels', (req, res) => { - Channel.find() - .sort('name') - .exec((err, channels) => { - res.render('index', { render: 'channels', channels: channels }) - }) -}) - -app.get('/channel/new', (req, res) => { - res.render('index', { - render: 'channel', - host: `${req.protocol}://${req.hostname}:${port}/` - }) -}) - -app.post('/channel/new', (req, res) => { - if (req.body.URLs) req.body.URLs = req.body.URLs.filter(u => u.trim() != '') - var c = new Channel(req.body) - c.save((err, channel) => { - if (err) console.log(err) - res.send(channel._id) - }) -}) - -app.get('/channel/:channel_id', (req, res) => { - Channel.findOne({ _id: req.params.channel_id }).exec((err, channel) => { - if (err) console.log(err) - if (channel) - res.render(`layouts/${channel.layout}`, { - channel: channel, - casting: false - }) - else res.render('layouts/empty', { casting: false }) - }) -}) - -app.post('/channel/:channel_id/edit', (req, res) => { - if (req.body.URLs) req.body.URLs = req.body.URLs.filter(u => u.trim() != '') - for (var i in devices) { - var d = devices[i] - - /* update channel info on local info for any devices displaying this channel */ - if ( - d.channel && - d.channel._id.toString() == req.params.channel_id.toString() - ) - Object.assign(devices[i].channel, req.body) - } - Channel.update( - { _id: req.params.channel_id }, - req.body, - (err, numAffected, response) => { - if (err) console.log(err) - res.send(req.params.channel_id) - } - ) -}) - -app.delete('/channel/:channel_id/edit', (req, res) => { - Channel.remove({ _id: req.params.channel_id }, () => { - for (var i in devices) { - var d = devices[i] - if ( - d.channel && - d.channel._id.toString() == req.params.channel_id.toString() - ) { - /* remove channel listing from local info of relevant devices */ - delete devices[i].channel - - /* send devices on this channel back to setup page */ - var c = clients.find( - c => stripIPv6(c.handshake.address) == devices[i].address - ) - if (c) c.emit('change_channel', null) - } - } - - /* remove channel listing from Mongo */ - Chromecast.update( - { channel: new mongoose.Types.ObjectId(req.params.channel_id) }, - { $unset: { channel: 1 } }, - (err, numAffected, result) => { - res.sendStatus(200) - } - ) - }) -}) - -app.get('/channel/:channel_id/edit', (req, res) => { - Channel.findOne({ _id: req.params.channel_id }).exec((err, channel) => { - if (err) console.log(err) - if (channel) - res.render('index', { - render: 'channel', - channel: channel, - host: `${req.protocol}://${req.hostname}:${port}/` - }) - else res.render('index', {}) - }) -}) - -/* Push Alerts & Takeovers */ - -app.get('/new/push', (req, res) => { - res.render('index', { render: 'push' }) -}) - -app.post('/new/push', (req, res) => { - clients.forEach(c => { - c.emit('push', { - message: req.body.message, - style: req.body.style, - duration: req.body.duration - }) - }) - res.sendStatus(200) -}) - -app.get('/new/takeover', (req, res) => { - Channel.find() - .sort('name') - .exec((err, channels) => { - res.render('index', { render: 'takeover', channels: channels }) - }) -}) - -app.post('/new/takeover', (req, res) => { - Channel.findOne({ - _id: new mongoose.Types.ObjectId(req.body.channel_id) - }).exec((err, channel) => { - if (err) console.log(err) - takeover = channel - clients.forEach(c => { - c.emit('change_channel', channel) - }) - res.sendStatus(200) - }) -}) +/* Push Alerts */ +app.use('/push', require('./routes/push')) -app.post('/takeover/end', (req, res) => { - takeover = null - clients.forEach(c => { - c.emit('refresh') - }) - res.sendStatus(200) -}) +/* Takeover */ +app.use('/takeover', require('./routes/takeover')) /* Server */ - server.listen(port, () => { console.log('MultiCast is live!') console.log(`listening at port ${port}...`) if (!serveOnly) { - /* load saved devices */ - Chromecast.find() - .populate('channel') - .exec((err, _devices) => { - for (var i in _devices) { - var d = _devices[i].toObject() - d.status = 'offline' - devices.push(d) - } - - /* poll for active devices */ - findDevices() - - /* start interval to continue polling for device status */ - setInterval(() => { - findDevices() - }, 30 * 1000) - }) + /* poll for active devices */ + devices.init() } }) - -/* Utility */ - -var stripIPv6 = ip => ip.replace(/^.*:/, '') diff --git a/app/routes/channels.js b/app/routes/channels.js new file mode 100644 index 0000000..4324670 --- /dev/null +++ b/app/routes/channels.js @@ -0,0 +1,55 @@ +const express = require('express') +const router = express.Router() + +const channels = require('../lib/channels') + +const Chromecast = require('../models/Chromecast') +const Channel = require('../models/Channel') + +const port = require('../lib/config').port + +router.use((req, res, next) => { + if (req.body.URLs) req.body.URLs = req.body.URLs.filter(u => u.trim() != '') + next() +}) + +router.get('/', (req, res) => { + res.render('index', { render: 'channels', channels: channels.list() }) +}) + +router + .route('/new') + .get((req, res) => { + res.render('index', { + render: 'channel', + host: `${req.protocol}://${req.hostname}:${port}/` + }) + }) + .post((req, res) => channels.create(req.body, id => res.send(id))) + +router.get('/:channel_id', (req, res) => { + let channel = channels.withId(req.params.channel_id) + if (channel) + res.render(`layouts/${channel.layout}`, { + channel: channel, + casting: false + }) + else res.render('layouts/empty', { casting: false }) +}) + +router + .route('/:channel_id/edit') + .get((req, res) => { + let channel = channels.withId(req.params.channel_id) + if (channel) + res.render('index', { + render: 'channel', + channel: channel, + host: `${req.protocol}://${req.hostname}:${port}/` + }) + else res.render('index', {}) + }) + .post((req, res) => channels.update(req.params.channel_id, req.body, id => res.send(id))) + .delete((req, res) => channels.remove(req.params.channel_id, () => res.sendStatus(200))) + +module.exports = router \ No newline at end of file diff --git a/app/routes/devices.js b/app/routes/devices.js new file mode 100644 index 0000000..76b1988 --- /dev/null +++ b/app/routes/devices.js @@ -0,0 +1,133 @@ +'use strict' + +const express = require('express') +const router = express.Router() + +const Chromecast = require('../models/Chromecast') + +const devices = require('../lib/devices') +const channels = require('../lib/channels') +const sockets = require('../lib/sockets') +const takeover = require('../lib/takeover') + +const port = require('../lib/config').port + +router.get('/', (req, res) => { + res.render('index', { render: 'devices', devices: devices.list() }) +}) + +router + .route('/new') + .get((req, res) => { + res.render('index', { + render: 'device', + devices: devices.list().filter(d => d.unregistered), + channels: channels.list() + }) + }) + .post((req, res) => { + let c = new Chromecast(req.body) + c.save((err, device) => { + if (err) console.log(err) + devices.register(device.deviceId, req.body) + res.send(device.deviceId) + }) + }) + +router.get('/:device_id', (req, res) => { + let d = devices.withId(req.params.device_id) + if (d && takeover.isActive()) { + res.render(`layouts/${takeover.channel().layout}`, { + deviceId: req.params.device_id, + channel: takeover.channel(), + casting: true + }) + } else { + if (d) { + if (d.channel) { + /* device registered and channel set + display device page */ + res.render(`layouts/${d.channel.layout}`, { + deviceId: req.params.device_id, + channel: d.channel, + casting: true + }) + } else { + /* device registered but no channel set + display setup page */ + res.render('setup-chromecast', { + device: d, + registered: true, + setupUrl: `${req.protocol}://${req.hostname}:${port}/` + }) + } + } else { + /* device is not registered + display setup page */ + res.render('setup-chromecast', { + device: d, + registered: false, + setupUrl: `${req.protocol}://${req.hostname}:${port}/` + }) + } + } +}) + +router.get('/:device_id/connect', (req, res) => { + let d = devices.withId(req.params.device_id) + if (!devices.isOnline(req.params.device_id)) devices.reconnect(d.address) + else { + // launch hub if not already open + // hard reload page if already open + var c = sockets.withHost(d.address) + if (c) c.emit('refresh') + } + res.sendStatus(200) +}) + +router + .route('/:device_id/edit') + .get((req, res) => { + let d = devices.withId(req.params.device_id) + if (d) { + res.render('index', { + render: 'device', + device: d, + channels: channels.list() + }) + } else render('index', {}) + }) + .post((req, res) => { + Chromecast.update( + { deviceId: req.params.device_id }, + req.body, + err => { + if (err) console.log(err) + + let d = devices.withId(req.params.device_id) + d.location = req.body.location // update local info with location + console.log('host', d.address) + let c = sockets.withHost(d.address), + channel = null + if (req.body.channel) { + channel = channels.withId(req.body.channel) + d.channel = channel // update local info with channel + } + console.log('socket_client', c) + if (c) c.emit('change_channel', channel) + res.send(req.params.device_id) + } + ) + }) + .delete((req, res) => { + Chromecast.remove({ deviceId: req.params.device_id }, () => { + /* Mark local info for device as unregistered */ + let d = devices.withId(req.params.device_id) + d.unregistered = true + delete d.channel + delete d.location + res.sendStatus(200) + }) + }) + +module.exports = router diff --git a/app/routes/message.js b/app/routes/message.js new file mode 100644 index 0000000..495ac74 --- /dev/null +++ b/app/routes/message.js @@ -0,0 +1,32 @@ +'use strict' + +const express = require('express') +const router = express.Router() +const path = require('path') +const fs = require('fs') +const devices = require('../lib/devices') +const messageFile = path.resolve(__dirname, '..', '..', 'message.txt') + +router.get('/', (req, res) => { + fs.readFile(messageFile, (err, data) => { + if (err || data == '') + res.render('message', { message: 'No message configured.' }) + else res.render('message', { message: data }) + }) +}) + +router + .route('/edit') + .get((req, res) => { + fs.readFile(messageFile, (err, data) => { + if (err) res.render('message-edit', { message: '' }) + else res.render('message-edit', { message: data }) + }) + }) + .post((req, res) => { + fs.writeFile(messageFile, req.body.message, err => console.log(err)) + devices.refreshChannelPath('message') + res.sendStatus(200) + }) + +module.exports = router \ No newline at end of file diff --git a/app/routes/push.js b/app/routes/push.js new file mode 100644 index 0000000..8a060fd --- /dev/null +++ b/app/routes/push.js @@ -0,0 +1,23 @@ +'use strict' + +const express = require('express') +const router = express.Router() + +const sockets = require('../lib/sockets') + +router.route('/') +.get((req, res) => { + res.render('index', { render: 'push' }) +}) +.post((req, res) => { + sockets.list().forEach(c => { + c.emit('push', { + message: req.body.message, + style: req.body.style, + duration: req.body.duration + }) + }) + res.sendStatus(200) +}) + +module.exports = router \ No newline at end of file diff --git a/app/routes/takeover.js b/app/routes/takeover.js new file mode 100644 index 0000000..5cadb72 --- /dev/null +++ b/app/routes/takeover.js @@ -0,0 +1,24 @@ +'use strict' + +const express = require('express') +const router = express.Router() +'use strict' + +const channels = require('../lib/channels') +const takeover = require('../lib/takeover') + +router + .route('/') + .get((req, res) => { + res.render('index', { render: 'takeover', channels: channels.list() }) + }) + .post((req, res) => { + console.log('activating takeover!') + takeover.activate(req.body.channel_id, () => res.sendStatus(200)) + }) + +router.post('/end', (req, res) => { + takeover.deactivate(() => res.sendStatus(200)) +}) + +module.exports = router \ No newline at end of file diff --git a/app/views/channels.pug b/app/views/channels.pug index 458ead1..139caa7 100644 --- a/app/views/channels.pug +++ b/app/views/channels.pug @@ -1,5 +1,5 @@ h1 Channels - a.btn.btn-primary.float-right(href='/channel/new') + a.btn.btn-primary.float-right(href='/channels/new') i.fa.fa-plus |   Add Channel table.table @@ -12,9 +12,9 @@ table.table each c in channels tr td - a(href=`/channel/${c._id}/edit`)= c.name + a(href=`/channels/${c._id}/edit`)= c.name td.text-right - a(href=`/channel/${c._id}`) Preview + a(href=`/channels/${c._id}`) Preview else tr td.text-gray(colspan=42) No channels available. \ No newline at end of file diff --git a/app/views/devices.pug b/app/views/devices.pug index f62f3b6..4fab704 100644 --- a/app/views/devices.pug +++ b/app/views/devices.pug @@ -1,5 +1,5 @@ h1 Chromecasts - a.btn.btn-primary.float-right(href='/device/new') + a.btn.btn-primary.float-right(href='/devices/new') i.fa.fa-plus |   Register Chromecast table.table @@ -14,21 +14,21 @@ table.table tr td if d.unregistered - a.badge(href='/device/new', class=d.status)= d.name + a.badge(href='/devices/new', class=d.status)= d.name div Unregistered else - a.badge(href=`/device/${d.deviceId}/edit`, class=d.status)= d.name + a.badge(href=`/devices/${d.deviceId}/edit`, class=d.status)= d.name div= d.location td if d.unregistered - | unregistered + span(style='color: #ccc') unregistered else if d.channel - a(href=`/channel/${d.channel._id}/edit`)= d.channel.name + a(href=`/channels/${d.channel._id}/edit`)= d.channel.name else .text-gray no channel td.text-right - a(href=`/device/${d.deviceId}`) Preview + a(href=`/devices/${d.deviceId}`) Preview else tr td.text-gray(colspan=42) No devices available. \ No newline at end of file diff --git a/app/views/home.pug b/app/views/home.pug index ef299da..cf5baf2 100644 --- a/app/views/home.pug +++ b/app/views/home.pug @@ -1,8 +1,8 @@ a.btn.btn-lg.btn-primary.btn-block(href='/devices') Chromecasts a.btn.btn-lg.btn-primary.btn-block(href='/channels') Channels -a.btn.btn-lg.btn-primary.btn-block(href='/new/push') Push Alert -unless takeover - a.btn.btn-lg.btn-primary.btn-block(href='/new/takeover') Channel Takeover +a.btn.btn-lg.btn-primary.btn-block(href='/push') Push Alert +unless takeover.isActive() + a.btn.btn-lg.btn-primary.btn-block(href='/takeover') Channel Takeover else a.btn.btn-lg.btn-error.btn-block#stop-takeover Stop Channel Takeover hr diff --git a/app/views/include/error.pug b/app/views/include/error.pug new file mode 100644 index 0000000..7b65e96 --- /dev/null +++ b/app/views/include/error.pug @@ -0,0 +1 @@ +#error(style='display: none') \ No newline at end of file diff --git a/app/views/include/nav.pug b/app/views/include/nav.pug index cdcaf64..1f02d2e 100644 --- a/app/views/include/nav.pug +++ b/app/views/include/nav.pug @@ -7,4 +7,17 @@ header.navbar.bg-success(style='padding: 0.3em') a.btn.btn-link(style='color: #fff', href='/devices') Devices a.btn.btn-link(style='color: #fff', href='/channels') Channels +if connection.hasErrors() + .toast.toast-error.connection-errors + strong= ux.error() + ul + each e in connection.getErrors() + each m in e.messages + li + strong= e.name + |   + i.fa.fa-angle-right + |   + = m + include toast.pug \ No newline at end of file diff --git a/app/views/layouts/fullscreen.pug b/app/views/layouts/fullscreen.pug index 6c0b87c..f6c04ba 100644 --- a/app/views/layouts/fullscreen.pug +++ b/app/views/layouts/fullscreen.pug @@ -6,6 +6,7 @@ html(lang='en') link(rel='stylesheet', href='/css/channel.css') body.fullscreen + include ../include/error.pug include ../include/toast.pug if channel.URLs != null && channel.URLs.length iframe(src=channel.URLs[0], frameborder=0, framespacing=0) diff --git a/app/views/layouts/right-panel.pug b/app/views/layouts/right-panel.pug index af4f658..603aeb0 100644 --- a/app/views/layouts/right-panel.pug +++ b/app/views/layouts/right-panel.pug @@ -6,6 +6,7 @@ html(lang='en') link(rel='stylesheet', href='/css/channel.css') body.right-panel + include ../include/error.pug include ../include/toast.pug if channel.URLS != null && channel.URLs.length iframe(src=channel.URLs[0], frameborder=0, framespacing=0) diff --git a/app/views/layouts/template.pug b/app/views/layouts/template.pug index 5a44efd..b0a6810 100644 --- a/app/views/layouts/template.pug +++ b/app/views/layouts/template.pug @@ -6,6 +6,7 @@ html(lang='en') link(rel='stylesheet', href='/css/channel.css') body.layout-name + include ../include/error.pug include ../include/toast.pug if channel.URLS != null && channel.URLs.length each u in channel.URLS diff --git a/build/css/channel.css b/build/css/channel.css index 46d598f..0998c06 100644 --- a/build/css/channel.css +++ b/build/css/channel.css @@ -1,4 +1,5 @@ -html, body { +html, +body { margin: 0; padding: 0; width: 100%; @@ -6,26 +7,48 @@ html, body { } body.fullscreen iframe { - width: 100vw; height: 100vh; + width: 100vw; + height: 100vh; } body.right-panel iframe:first-of-type { position: absolute; - top: 0; left: 0; - width: 80vw; height: 100vh; + top: 0; + left: 0; + width: 80vw; + height: 100vh; } body.right-panel iframe:nth-of-type(2) { position: absolute; - top: 0; left: 80vw; - width: 20vw; height: 100vh; + top: 0; + left: 80vw; + width: 20vw; + height: 100vh; +} + +#error { + position: absolute; + top: 0; + left: 0; + width: 100vw; + background: #000; + color: #fff; + padding: 20px; +} + +#error span { + font-weight: 700; + color: #f00; } #toast.full-width { width: calc(100% - 20px); - left: 10px; bottom: 15px; right: 10px; + left: 10px; + bottom: 15px; + right: 10px; } #toast.full-width .title { font-size: 7.5vh; -} \ No newline at end of file +} diff --git a/build/css/main.css b/build/css/main.css index a4fb127..8db2d2f 100644 --- a/build/css/main.css +++ b/build/css/main.css @@ -1,4 +1,5 @@ -html, body { +html, +body { background: #f8f9fa; } @@ -6,7 +7,8 @@ html, body { width: 400px; max-width: 100%; position: fixed; - bottom: 20px; right: 20px; + bottom: 20px; + right: 20px; } .toast .title, @@ -14,14 +16,20 @@ html, body { margin: 0; } -.toast .title + .text { - margin-top: .25em; +.toast .title + .text { + margin-top: 0.25em; } -.toast :empty { +.toast:empty { display: none; } +.connection-errors > ul { + list-style: none; + margin: 0; + padding: 0; +} + #wrapper { background: #fff; padding: 3em; @@ -44,7 +52,7 @@ html, body { .btn-error:hover, .btn-error:focus, .btn-error:active { - border-color: #e85600; + border-color: #e85600; background: #df5200; color: #fff; } @@ -74,4 +82,4 @@ footer { width: 100%; min-height: 100px; padding: 3em; -} \ No newline at end of file +} diff --git a/build/js/app.js b/build/js/app.js index eda9053..db32180 100644 --- a/build/js/app.js +++ b/build/js/app.js @@ -3,17 +3,53 @@ function notify(opts) { if (notifyIsActive) return notifyIsActive = true var title = opts.title || '', - msg = opts.message || '', - style = opts.style != '' ? `toast-${opts.style}` : '', - full = opts.fullWidth || false, - dur = opts.duration || 3000 - $('#toast').find('.title').text(title).end() - .find('.text').text(msg).end() - .addClass(`${style}${full ? ' full-width' : ''}`).fadeIn() + msg = opts.message || '', + style = opts.style != '' ? `toast-${opts.style}` : '', + full = opts.fullWidth || false, + dur = opts.duration || 3000 + $('#toast') + .find('.title') + .text(title) + .end() + .find('.text') + .text(msg) + .end() + .addClass(`${style}${full ? ' full-width' : ''}`) + .fadeIn() setTimeout(function() { $('#toast').fadeOut(500, function() { $('#toast').removeClass(`${style} full-width`) notifyIsActive = false }) }, dur) -} \ No newline at end of file +} + +function displayError(message) { + $('#error') + .append( + `
+ Error! + ${message} +
` + ) + .show() +} + +function checkFrame(frame) { + var html, + failed = false + try { + var doc = frame.contentDocument || frame.contentWindow.document + html = doc.body.innerHTML + } catch (e) { + failed = true + } + + if (!html) failed = true + if (failed) + displayError('Frame loading blocked due to cross-origin prevention') +} + +$('iframe').each(function(i) { + checkFrame(this) +}) diff --git a/build/js/client.js b/build/js/client.js index 34ca92e..17158dd 100644 --- a/build/js/client.js +++ b/build/js/client.js @@ -1,7 +1,9 @@ /* socket.io */ var socket = io() -socket.on('connect', function () { }) +console.log('establishing socket.io connection') + +socket.on('connect', function () { console.log('connection established!') }) socket.on('disconnect', function() { }) socket.on('refresh', function() { location.reload(true) }) diff --git a/index.js b/index.js index ef2a609..6c442cb 100644 --- a/index.js +++ b/index.js @@ -10,8 +10,14 @@ if (process.argv.length == 2) { // r console.log(' start start Multicast as a foreground process') console.log('') console.log('Flags:') + console.log(' -v, --version print application version') console.log(' --serve-only do not run the mDNS server (won\'t interrupt existing receivers)') } else { + if (process.argv.find(arg => arg == '-v' || arg == '--version')) { + console.log(`Multicast v${require('./package.json').version}`) + console.log('Author: Aaron Ross (@superhawk610)') + process.exit(0) + } if (process.argv.find(arg => arg == 'config')) require('./app/config.js') // run configuration else require('./app/main.js') // start application } diff --git a/package-lock.json b/package-lock.json index 3f7d5f3..95a2711 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,15 @@ { "name": "multicast", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/node": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.2.tgz", + "integrity": "sha512-KA4GKOpgXnrqEH2eCVhiv2CsxgXGQJgV1X0vsGlh+WCnxbeAE1GT44ZsTU1IN5dEeV/gDupKa7gWo08V5IxWVQ==", + "dev": true + }, "accepts": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", @@ -71,6 +77,15 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, "arr-diff": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", @@ -109,6 +124,15 @@ "integrity": "sha1-5zA08A3MH0CHYAj9IP6ud71LfC8=", "dev": true }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, "array-uniq": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", @@ -153,11 +177,34 @@ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, + "babel-code-frame": { + "version": "7.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-7.0.0-alpha.12.tgz", + "integrity": "sha512-5BmA2es52XNA9PN96HRfJg0co5CkmrKxWdvyW507LNqJmicnHM4we4HI16bIzgAuVeL7RKc4GJmTsyhkFco4Tw==", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babylon": { + "version": "7.0.0-beta.28", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.28.tgz", + "integrity": "sha512-DBCCAejmP2ub9aCxA+ZN+Yv67Z/9yQqs4crzo9IuJxqbCNpR0KnvyzQxoB1S9G/R0vnAonPT0osaKjikuCAWqQ==", + "dev": true + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" }, + "bail": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.2.tgz", + "integrity": "sha1-99bBcxYwqfnw1NNe0fli4gdKF2Q=", + "dev": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -311,6 +358,18 @@ "supports-color": "2.0.0" } }, + "character-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.1.tgz", + "integrity": "sha1-92hxvl72bdt/j440eOzDdMJ9bco=", + "dev": true + }, + "character-entities-legacy": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.1.tgz", + "integrity": "sha1-9Ad53xoQGHK7UQo9KV4fzPFHIC8=", + "dev": true + }, "character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", @@ -319,6 +378,18 @@ "is-regex": "1.0.4" } }, + "character-reference-invalid": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.1.tgz", + "integrity": "sha1-lCg191Dk7GGjCOYMLvjMEBEgLvw=", + "dev": true + }, + "cjk-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cjk-regex/-/cjk-regex-1.0.2.tgz", + "integrity": "sha512-NwSMtwULPLk8Ka9DEUcoFXhMRnV/bpyKDnoyDiVw/Qy5przhvHTvXLcsKaOmx13o8J4XEsPVT1baoCUj5zQs3w==", + "dev": true + }, "clean-css": { "version": "3.4.28", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", @@ -350,6 +421,27 @@ "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", "dev": true }, + "collapse-white-space": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.3.tgz", + "integrity": "sha1-S5BvZw5aljqHt2sOFolkM0G2Ajw=", + "dev": true + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "colour": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", @@ -418,6 +510,24 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cosmiconfig": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-3.1.0.tgz", + "integrity": "sha512-zedsBhLSbPBms+kE7AH4vHg6JsKDz6epSv2/+5XHs8ILHlgDciSJfSWf8sX9aQ52Jb7KI7VswUTsLpR/G0cr2Q==", + "dev": true, + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.10.0", + "parse-json": "3.0.0", + "require-from-string": "2.0.1" + } + }, + "dashify": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dashify/-/dashify-0.2.2.tgz", + "integrity": "sha1-agdBWgHJH69KMuONnfunH2HLIP4=", + "dev": true + }, "dateformat": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", @@ -471,6 +581,18 @@ "fs-exists-sync": "0.1.0" } }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -516,6 +638,12 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "emoji-regex": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", + "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==", + "dev": true + }, "encodeurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", @@ -585,6 +713,15 @@ "has-binary2": "1.0.2" } }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, "es6-promise": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", @@ -601,6 +738,18 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -707,6 +856,15 @@ "time-stamp": "1.1.0" } }, + "fault": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.1.tgz", + "integrity": "sha1-3o01Df1IviS13BsChn4IcbkTUJI=", + "dev": true, + "requires": { + "format": "0.2.2" + } + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -801,6 +959,18 @@ "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=", "dev": true }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "dev": true + }, + "flow-parser": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.59.0.tgz", + "integrity": "sha1-9uvK5h/6GH5CCZnUDOCoAfObJjU=", + "dev": true + }, "font-awesome": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", @@ -821,6 +991,12 @@ "for-in": "1.0.2" } }, + "format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", + "dev": true + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -837,6 +1013,12 @@ "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", "dev": true }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -851,6 +1033,12 @@ "globule": "0.1.0" } }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, "glob": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", @@ -972,6 +1160,44 @@ "which": "1.3.0" } }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.3.3", + "path-is-absolute": "1.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, "globule": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", @@ -1047,6 +1273,15 @@ "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" }, + "graphql": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.10.5.tgz", + "integrity": "sha512-Q7cx22DiLhwHsEfUnUip1Ww/Vfx7FS0w6+iHItNuN61+XpegHSa3k5U0+6M5BcpavQImBwFiy0z3uYwY7cXMLQ==", + "dev": true, + "requires": { + "iterall": "1.1.3" + } + }, "gulp": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.9.1.tgz", @@ -1231,6 +1466,12 @@ "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, "has-gulplog": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", @@ -1270,6 +1511,18 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" }, + "ignore": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -1317,11 +1570,45 @@ "is-windows": "0.2.0" } }, + "is-alphabetical": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.1.tgz", + "integrity": "sha1-x3B5zJHU76x3W+EDS/LSQ/lebwg=", + "dev": true + }, + "is-alphanumerical": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.1.tgz", + "integrity": "sha1-37SqTRCF4zvbYcLe6cgOnGwZ9Ts=", + "dev": true, + "requires": { + "is-alphabetical": "1.0.1", + "is-decimal": "1.0.1" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "is-decimal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.1.tgz", + "integrity": "sha1-9ftqlJlq2ejjdh+/vQkfH8qMToI=", + "dev": true + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, "is-dotfile": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", @@ -1358,6 +1645,12 @@ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", "dev": true }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, "is-glob": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", @@ -1367,6 +1660,12 @@ "is-extglob": "1.0.0" } }, + "is-hexadecimal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz", + "integrity": "sha1-bghLvJIGH7sJcexYts5tQE4k2mk=", + "dev": true + }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -1376,6 +1675,12 @@ "kind-of": "3.2.2" } }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -1442,12 +1747,24 @@ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, + "is-whitespace-character": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.1.tgz", + "integrity": "sha1-muAXbzKCtlRXoZks2whPil+DPjs=", + "dev": true + }, "is-windows": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", "dev": true }, + "is-word-character": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.1.tgz", + "integrity": "sha1-WgP6HqkazopusMfNdw64bWXIvvs=", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1468,16 +1785,102 @@ "isarray": "1.0.0" } }, + "iterall": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.1.3.tgz", + "integrity": "sha512-Cu/kb+4HiNSejAPhSaN1VukdNTTi/r4/e+yykqjlG/IW+1gZH5b4+Bq3whDX4tvbYugta3r8KTMUiqT3fIGxuQ==", + "dev": true + }, + "jest-docblock": { + "version": "21.3.0-beta.7", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.3.0-beta.7.tgz", + "integrity": "sha512-Qk4si4BSb79ZNUsxYHGBXHf+nIUD9fbgfkipow+WGL8HSwp1cDOU1i127MC5kIqjoU7h9HOZcJlzBKcip4uizQ==", + "dev": true, + "requires": { + "detect-newline": "2.1.0" + } + }, + "jest-get-type": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-21.2.0.tgz", + "integrity": "sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q==", + "dev": true + }, + "jest-validate": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-21.1.0.tgz", + "integrity": "sha512-xS0cyErNWpsLFlGkn/b87pk/Mv7J+mCTs8hQ4KmtOIIoM1sHYobXII8AtkoN8FC7E3+Ptxjo+/3xWk6LK1dKcw==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "jest-get-type": "21.2.0", + "leven": "2.1.0", + "pretty-format": "21.2.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "jquery": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz", "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" }, + "js-base64": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz", + "integrity": "sha512-Wehd+7Pf9tFvGb+ydPm9TjYjV8X1YHOVyG8QyELZxEMqOhemVwGRmoG8iQ/soqI3n8v4xn59zaLxiCJiaaRzKA==", + "dev": true + }, "js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, "jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", @@ -1505,6 +1908,12 @@ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, "liftoff": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.3.0.tgz", @@ -1669,6 +2078,12 @@ "lodash.escape": "3.2.0" } }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, "long": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", @@ -1691,6 +2106,12 @@ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true }, + "markdown-escapes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.1.tgz", + "integrity": "sha1-GZTfLTr0gR3lmmcUk0wrIpJzRRg=", + "dev": true + }, "mdns": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/mdns/-/mdns-2.3.4.tgz", @@ -1705,6 +2126,15 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.1.0" + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -1754,6 +2184,12 @@ "mime-db": "1.30.0" } }, + "mimic-fn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", + "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", + "dev": true + }, "minimatch": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", @@ -2014,6 +2450,20 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, + "parse-entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.1.1.tgz", + "integrity": "sha1-gRLYhHExnyerrk1klksSL+ThuJA=", + "dev": true, + "requires": { + "character-entities": "1.2.1", + "character-entities-legacy": "1.1.1", + "character-reference-invalid": "1.1.1", + "is-alphanumerical": "1.0.1", + "is-decimal": "1.0.1", + "is-hexadecimal": "1.0.1" + } + }, "parse-filepath": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.1.tgz", @@ -2037,12 +2487,30 @@ "is-glob": "2.0.1" } }, + "parse-json": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-3.0.0.tgz", + "integrity": "sha1-+m9HsY4jgm6tMvJj50TQ4ehH+xM=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "dev": true }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "dev": true, + "requires": { + "@types/node": "8.5.2" + } + }, "parseqs": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", @@ -2064,6 +2532,12 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, "path-parse": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", @@ -2089,12 +2563,277 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "dev": true, + "requires": { + "chalk": "1.1.3", + "js-base64": "2.4.0", + "source-map": "0.5.7", + "supports-color": "3.2.3" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-less": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-1.1.1.tgz", + "integrity": "sha512-zl0EEqq8Urh37Ppdv9zzhpZpLHrgkxmt6e3O4ftRa7/b8Uq2LV+/KBVM8/KuzmHNu+mthhOArg1lxbfqQ3NUdg==", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", + "dev": true + }, + "postcss-scss": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-1.0.2.tgz", + "integrity": "sha1-/0XPM1S4ee6JpOtoaA9GrJuxT5Q=", + "dev": true, + "requires": { + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "1.0.2", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + }, + "postcss-values-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-1.3.1.tgz", + "integrity": "sha512-chFn9CnFAAUpQ3cwrxvVjKB8c0y6BfONv6eapndJoTXJ3h8fr1uAiue8lGP3rUIpBI2KgJGdgCVk9KNvXh0n6A==", + "dev": true, + "requires": { + "flatten": "1.0.2", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + }, "preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", "dev": true }, + "prettier": { + "version": "git+https://github.com/superhawk610/prettier.git#be4bf4467857ad7f3ca45901dac48a9fcc61c521", + "dev": true, + "requires": { + "babel-code-frame": "7.0.0-alpha.12", + "babylon": "7.0.0-beta.28", + "camelcase": "4.1.0", + "chalk": "2.1.0", + "cjk-regex": "1.0.2", + "cosmiconfig": "3.1.0", + "dashify": "0.2.2", + "diff": "3.2.0", + "emoji-regex": "6.5.1", + "escape-string-regexp": "1.0.5", + "esutils": "2.0.2", + "flow-parser": "0.59.0", + "get-stream": "3.0.0", + "globby": "6.1.0", + "graphql": "0.10.5", + "ignore": "3.3.7", + "jest-docblock": "21.3.0-beta.7", + "jest-validate": "21.1.0", + "leven": "2.1.0", + "mem": "1.1.0", + "minimatch": "3.0.4", + "minimist": "1.2.0", + "parse5": "3.0.3", + "postcss-less": "1.1.1", + "postcss-media-query-parser": "0.2.3", + "postcss-scss": "1.0.2", + "postcss-selector-parser": "2.2.3", + "postcss-values-parser": "1.3.1", + "remark-frontmatter": "1.1.0", + "remark-parse": "4.0.0", + "semver": "5.4.1", + "string-width": "2.1.1", + "typescript": "2.5.3", + "typescript-eslint-parser": "git://github.com/eslint/typescript-eslint-parser.git#9c71a627da36e97da52ed2731d58509c952b67ae", + "unicode-regex": "1.0.1", + "unified": "6.1.5" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "pretty-format": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-21.2.1.tgz", + "integrity": "sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A==", + "dev": true, + "requires": { + "ansi-regex": "3.0.0", + "ansi-styles": "3.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + } + } + }, "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -2361,6 +3100,39 @@ "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" }, + "remark-frontmatter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-1.1.0.tgz", + "integrity": "sha512-mLbYtwP9w1L9TA8dX+I/HyDF5lCpa0dmYvvW9Io+zUPpqEZ49QMKWb0hSpunpLVA+Squy0SowzSzjHVPbxWq1g==", + "dev": true, + "requires": { + "fault": "1.0.1", + "xtend": "4.0.1" + } + }, + "remark-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-4.0.0.tgz", + "integrity": "sha512-XZgICP2gJ1MHU7+vQaRM+VA9HEL3X253uwUM/BGgx3iv6TH2B3bF3B8q00DKcyP9YrJV+/7WOWEWBFF/u8cIsw==", + "dev": true, + "requires": { + "collapse-white-space": "1.0.3", + "is-alphabetical": "1.0.1", + "is-decimal": "1.0.1", + "is-whitespace-character": "1.0.1", + "is-word-character": "1.0.1", + "markdown-escapes": "1.0.1", + "parse-entities": "1.1.1", + "repeat-string": "1.6.1", + "state-toggle": "1.0.0", + "trim": "0.0.1", + "trim-trailing-lines": "1.1.0", + "unherit": "1.1.0", + "unist-util-remove-position": "1.1.1", + "vfile-location": "2.0.2", + "xtend": "4.0.1" + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -2384,6 +3156,12 @@ "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", "dev": true }, + "require-from-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.1.tgz", + "integrity": "sha1-xUUjPp19pmFunVmt+zn8n1iGdv8=", + "dev": true + }, "require_optional": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", @@ -2568,6 +3346,18 @@ "resolved": "https://registry.npmjs.org/spectre.css/-/spectre.css-0.4.6.tgz", "integrity": "sha1-HfmK0Qk/rpDebHSHlmDVnNAphU8=" }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "state-toggle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.0.tgz", + "integrity": "sha1-0g+aYWu08MO5i5GSLSW2QKorxCU=", + "dev": true + }, "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", @@ -2579,6 +3369,33 @@ "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=", "dev": true }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", @@ -2647,6 +3464,24 @@ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" }, + "trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", + "dev": true + }, + "trim-trailing-lines": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.0.tgz", + "integrity": "sha1-eu+7eAjfnWafbaLkOMrIxGradoQ=", + "dev": true + }, + "trough": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.1.tgz", + "integrity": "sha1-qf2LA5Swro//guBjOgo2zK1bX4Y=", + "dev": true + }, "type-is": { "version": "1.6.15", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", @@ -2656,6 +3491,20 @@ "mime-types": "2.1.17" } }, + "typescript": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", + "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", + "dev": true + }, + "typescript-eslint-parser": { + "version": "git://github.com/eslint/typescript-eslint-parser.git#9c71a627da36e97da52ed2731d58509c952b67ae", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.4.1" + } + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -2690,12 +3539,79 @@ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", "dev": true }, + "unherit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.0.tgz", + "integrity": "sha1-a5qu379z3xdWrZ4xbdmBiFhAzX0=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "xtend": "4.0.1" + } + }, + "unicode-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unicode-regex/-/unicode-regex-1.0.1.tgz", + "integrity": "sha1-+BngUBkdW5VhozmljdO5CV7ZSzU=", + "dev": true + }, + "unified": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-6.1.5.tgz", + "integrity": "sha1-cWk3hyYhpjE15iztLzrGoGPG+4c=", + "dev": true, + "requires": { + "bail": "1.0.2", + "extend": "3.0.1", + "is-plain-obj": "1.1.0", + "trough": "1.0.1", + "vfile": "2.3.0", + "x-is-function": "1.0.4", + "x-is-string": "0.1.0" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, "unique-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz", "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=", "dev": true }, + "unist-util-is": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.1.tgz", + "integrity": "sha1-DDEmKeP5YMZukx6BLT2A53AQlHs=", + "dev": true + }, + "unist-util-remove-position": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.1.tgz", + "integrity": "sha1-WoXBVV/BugwQG4ZwfRXlD6TIcbs=", + "dev": true, + "requires": { + "unist-util-visit": "1.3.0" + } + }, + "unist-util-stringify-position": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.1.tgz", + "integrity": "sha1-PMvcU2ee7W7PN3fdf14yKcG2qjw=", + "dev": true + }, + "unist-util-visit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.3.0.tgz", + "integrity": "sha512-9ntYcxPFtl44gnwXrQKZ5bMqXMY0ZHzUpqMFiU4zcc8mmf/jzYm8GhYgezuUlX4cJIM1zIDYaO6fG/fI+L6iiQ==", + "dev": true, + "requires": { + "unist-util-is": "2.1.1" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2737,6 +3653,41 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "vfile": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", + "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", + "dev": true, + "requires": { + "is-buffer": "1.1.6", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "1.1.1", + "vfile-message": "1.0.0" + }, + "dependencies": { + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + } + } + }, + "vfile-location": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.2.tgz", + "integrity": "sha1-02dcWch3SY5JK0dW/2Xkrxp1IlU=", + "dev": true + }, + "vfile-message": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.0.0.tgz", + "integrity": "sha512-HPREhzTOB/sNDc9/Mxf8w0FmHnThg5CRSJdR9VRFkD2riqYWs+fuXlj5z8mIpv2LrD7uU41+oPWFOL4Mjlf+dw==", + "dev": true, + "requires": { + "unist-util-stringify-position": "1.1.1" + } + }, "vinyl": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", @@ -2882,6 +3833,18 @@ "ultron": "1.1.1" } }, + "x-is-function": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/x-is-function/-/x-is-function-1.0.4.tgz", + "integrity": "sha1-XSlNw9Joy90GJYDgxd93o5HR+h4=", + "dev": true + }, + "x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", + "dev": true + }, "xmlhttprequest-ssl": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz", diff --git a/package.json b/package.json index 3ba43f2..2c4a7bb 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "multicast", - "version": "1.0.2", - "description": "Service for persisting a session indefinitely across multiple Chromecast devices.", + "version": "1.1.0", + "description": + "Service for persisting a session indefinitely across multiple Chromecast devices.", "main": "index.js", "scripts": { "prepublishOnly": "gulp", "start": "node .", - "test": "echo \"No tests configured\"" + "test": "echo \"No tests configured\" && exit 0" }, "bin": { "multicast": "index.js" @@ -15,14 +16,9 @@ "type": "git", "url": "git+https://github.com/superhawk610/multicast.git" }, - "keywords": [ - "chrome", - "chromecast", - "multiple", - "screens", - "persist" - ], - "author": "Aaron Ross (https://github.com/superhawk610)", + "keywords": ["chrome", "chromecast", "multiple", "screens", "persist"], + "author": + "Aaron Ross (https://github.com/superhawk610)", "license": "ISC", "bugs": { "url": "https://github.com/superhawk610/multicast/issues"