Merge branch 'add-electron-app' into service-testing

canary
Bronwen 2021-04-11 20:00:07 -04:00
commit ec6b3a068b
11 changed files with 106 additions and 208 deletions

View File

@ -15,19 +15,19 @@
"productName": "ElectronReact", "productName": "ElectronReact",
"description": "Electron application boilerplate based on React, React Router, Webpack, React Fast Refresh for rapid application development", "description": "Electron application boilerplate based on React, React Router, Webpack, React Fast Refresh for rapid application development",
"scripts": { "scripts": {
"postinstall": "node -r @babel/register .erb/scripts/CheckNativeDep.js && electron-builder install-app-deps && yarn cross-env NODE_ENV=development webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.babel.js && opencollective-postinstall && yarn-deduplicate yarn.lock",
"build": "concurrently \"yarn build:main\" \"yarn build:renderer\"", "build": "concurrently \"yarn build:main\" \"yarn build:renderer\"",
"build:main": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.main.prod.babel.js", "build:main": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.main.prod.babel.js",
"build:renderer": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.renderer.prod.babel.js", "build:renderer": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.renderer.prod.babel.js",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir src", "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir src",
"lint": "cross-env NODE_ENV=development eslint . --cache --ext .js,.jsx,.ts,.tsx", "lint": "cross-env NODE_ENV=development eslint . --cache --ext .js,.jsx,.ts,.tsx",
"package": "rm -rf src/dist && yarn build && electron-builder build --publish never", "package": "rm -rf src/dist && yarn build && electron-builder build --publish never",
"postinstall": "node -r @babel/register .erb/scripts/CheckNativeDep.js && electron-builder install-app-deps && yarn cross-env NODE_ENV=development webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.babel.js && opencollective-postinstall && yarn-deduplicate yarn.lock",
"start": "node -r @babel/register ./.erb/scripts/CheckPortInUse.js && cross-env yarn start:renderer", "start": "node -r @babel/register ./.erb/scripts/CheckPortInUse.js && cross-env yarn start:renderer",
"start:main": "cross-env NODE_ENV=development electron -r ./.erb/scripts/BabelRegister ./src/main.dev.ts", "start:main": "cross-env NODE_ENV=development electron -r ./.erb/scripts/BabelRegister ./src/main.dev.ts",
"start:renderer": "cross-env NODE_ENV=development webpack serve --config ./.erb/configs/webpack.config.renderer.dev.babel.js", "start:renderer": "cross-env NODE_ENV=development webpack serve --config ./.erb/configs/webpack.config.renderer.dev.babel.js",
"test": "jest", "test": "jest",
"relay": "supervisor -w relay,src -n exit relay/index.js", "relay": "supervisor -w relay,src -n exit relay/index.mjs",
"relay:service": "supervisor -- relay/service", "relay:service": "supervisor -- relay/service.mjs",
"sloc": "find lib -type f | xargs wc -l" "sloc": "find lib -type f | xargs wc -l"
}, },
"build": { "build": {
@ -260,6 +260,7 @@
"md5": "^2.3.0", "md5": "^2.3.0",
"menubar": "^9.0.3", "menubar": "^9.0.3",
"nat-upnp": "^1.1.1", "nat-upnp": "^1.1.1",
"nedb": "^1.8.0",
"node-ipc": "^9.1.4", "node-ipc": "^9.1.4",
"node-rsa": "^1.1.1", "node-rsa": "^1.1.1",
"printable-characters": "^1.0.42", "printable-characters": "^1.0.42",

View File

@ -1,21 +1,26 @@
// process.env.DEBUG = 'xyz:valnet:*'; // process.env.DEBUG = 'xyz:valnet:*';
'use strict';
import { title } from '../src/lib/title.js';
import { Identity } from '../src/lib/Identity.js';
import { config } from '../src/lib/config/index.js';
import appdata from '../src/lib/appdata.js';
import Node from '../src/lib/node.js';
import Signale from 'signale';
import { ensureDirSync } from 'fs-extra';
import express from 'express';
(async () => {
const { title } = require('../src/lib/title');
const log = require('signale').scope('RLAY');
const { Identity } = require('../src/lib/Identity');
title('relay', false); title('relay', false);
const Node = require('../src/lib/node'); const log = Signale.scope('RLAY');
const { config } = require('../src/lib/config');
const { ensureDirSync } = require('fs-extra');
const appdata = require('../src/lib/appdata');
ensureDirSync(`${appdata}/valnet/relay`);
const node = new Node(); const node = new Node();
(async () => {
// ==================================== [EXPRESS] // ==================================== [EXPRESS]
const express = require('express');
const app = express(); const app = express();
ensureDirSync(`${appdata}/valnet/relay`);
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.end(` res.end(`

View File

@ -1,21 +1,24 @@
import Signale from 'signale';
import { execSync, spawn } from 'child_process';
import Datastore from 'nedb';
import { config } from '../src/lib/config/index.js';
import express from 'express';
(async () => { (async () => {
const log = require('signale').scope('SRVC');
const { execSync, spawn } = require('child_process'); const log = Signale.scope('SRVC');
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
let proc; let proc;
const Datastore = require('nedb');
const logs = new Datastore({ const logs = new Datastore({
filename: 'svc.log', filename: 'svc.log',
autoload: true autoload: true
}); });
const { config } = require('../package.json');
const express = require('express');
const app = express(); const app = express();
try { try {
appendLogs('yarn', execSync(`yarn`)); appendLogs('yarn', execSync(`yarn`));
} catch (e) { } catch (e) {
appendLogs('failed to yarn install...') logp('failed to yarn install...')
} }
logp('=================================='); logp('==================================');
@ -47,7 +50,7 @@ setInterval(function update() {
}, 5000); }, 5000);
(function keepAlive() { (function keepAlive() {
proc = spawn('node', ['relay'], { proc = spawn('node', ['./relay/index.mjs'], {
stdio: 'pipe' stdio: 'pipe'
}); });

View File

@ -2,7 +2,8 @@ import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import icon from '../assets/icon.svg'; import icon from '../assets/icon.svg';
import './App.global.css'; import './App.global.css';
import { IpcClient } from './lib/ipc'; import { IpcClient } from './lib/ipc.js';
class Hello extends React.Component { class Hello extends React.Component {
api = new IpcClient('valnet'); api = new IpcClient('valnet');
@ -15,6 +16,11 @@ class Hello extends React.Component {
console.log(await this.api.getClients()) console.log(await this.api.getClients())
} }
async killApp() {
// alert(this);
await this.api.kill(1);
}
render() { render() {
return ( return (
<div> <div>
@ -24,20 +30,19 @@ class Hello extends React.Component {
<h1>electron-react-boilerplate</h1> <h1>electron-react-boilerplate</h1>
<div className="Hello"> <div className="Hello">
<a <a
href="https://electron-react-boilerplate.js.org/" href="#"
target="_blank"
rel="noreferrer" rel="noreferrer"
onClick={this.killApp.bind(this)}
> >
<button type="button"> <button type="button">
<span role="img" aria-label="books"> <span role="img" aria-label="books">
📚 📚
</span> </span>
Read our docs Kill App
</button> </button>
</a> </a>
<a <a
href="https://github.com/sponsors/electron-react-boilerplate" target="#"
target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<button type="button"> <button type="button">

View File

@ -5,7 +5,6 @@ module.exports.StreamJsonParser = class StreamJsonParser extends EventEmitter {
buffer = ''; buffer = '';
data(evt) { data(evt) {
console.log(evt)
// toString it, in case its a buffer! // toString it, in case its a buffer!
this.buffer += evt.toString(); this.buffer += evt.toString();
this.processBuffer(); this.processBuffer();
@ -15,8 +14,6 @@ module.exports.StreamJsonParser = class StreamJsonParser extends EventEmitter {
const parts = this.buffer.split(/(\x02[^\x02\x03]*\x03)/g); const parts = this.buffer.split(/(\x02[^\x02\x03]*\x03)/g);
this.buffer = ''; this.buffer = '';
console.log('process', this.buffer);
for(const message of parts) { for(const message of parts) {
if(message.endsWith('\x03')) { if(message.endsWith('\x03')) {
const obj = JSON.parse(message.substr(1, message.length - 2)); const obj = JSON.parse(message.substr(1, message.length - 2));

View File

@ -0,0 +1,12 @@
module.exports = {
"ports": {
"relay": 5600,
"relayEnd": 5699,
"http": 5700,
"service": 5000
},
"endpoints": [
"valnet.xyz:5500",
"35.196.210.135:5600"
]
}

View File

@ -1,9 +1,9 @@
const { readFileSync, writeFileSync, existsSync } = require('fs'); const { readFileSync, writeFileSync, existsSync } = require('fs');
const { ensureDirSync } = require('fs-extra'); const { ensureDirSync } = require('fs-extra');
const { config } = require('../../package.json'); const config = require('./defaults.js');
const deepmerge = require('deepmerge'); const deepmerge = require('deepmerge');
const appdata = require('./appdata'); const appdata = require('../appdata');
ensureDirSync(`${appdata}/valnet/relay`); ensureDirSync(`${appdata}/valnet/relay`);
const filepath = `${appdata}/valnet/relay/config.json`; const filepath = `${appdata}/valnet/relay/config.json`;

View File

@ -28,8 +28,6 @@ class IpcServer extends EventEmitter {
ipc.server.on('req', (evt, incommingSocket) => { ipc.server.on('req', (evt, incommingSocket) => {
if (incommingSocket !== socket) return; if (incommingSocket !== socket) return;
console.log('req -', evt);
const [ const [
name, name,
...args ...args
@ -82,12 +80,8 @@ function IpcClient(name) {
return this.proxy; return this.proxy;
} }
if(!!module.exports) {
module.exports.IpcServer = IpcServer;
module.exports.IpcClient = IpcClient;
}
export { module.exports = {
IpcServer, IpcServer,
IpcClient IpcClient
} };

View File

@ -2,8 +2,7 @@ const EventEmitter = require('events')
const stp = require('./STP'); const stp = require('./STP');
const upnp = require('./upnp'); const upnp = require('./upnp');
const md5 = require('md5'); const md5 = require('md5');
const pkg = require('./../package.json'); const { config, write } = require('./config/index.js');
const { config, write } = require('./config.js');
const log = require('signale').scope('NODE'); const log = require('signale').scope('NODE');
const bonjour = require('bonjour')(); const bonjour = require('bonjour')();
const Gateway = require('./Gateway'); const Gateway = require('./Gateway');
@ -61,7 +60,7 @@ class Node extends EventEmitter {
log.info('incomming connection from ' + connection.remoteName); log.info('incomming connection from ' + connection.remoteName);
}); });
log.info('advertising node on multicast...') log.info('advertising node on multicast...');
this.multicastAd = bonjour.publish({ this.multicastAd = bonjour.publish({
name: this.name, name: this.name,
type: 'stp', type: 'stp',
@ -76,6 +75,7 @@ class Node extends EventEmitter {
const ipcServer = new IpcServer('valnet'); const ipcServer = new IpcServer('valnet');
ipcServer.registerFunction(this.getClients.bind(this)); ipcServer.registerFunction(this.getClients.bind(this));
ipcServer.registerFunction(this.kill.bind(this));
// log.success('Node successfully registered!'); // log.success('Node successfully registered!');
} }
@ -85,6 +85,10 @@ class Node extends EventEmitter {
return this.clients; return this.clients;
} }
kill(code) {
process.exit(code);
}
async serviceUp(device) { async serviceUp(device) {
this.multicastDevices.push(device); this.multicastDevices.push(device);
} }
@ -120,8 +124,6 @@ class Node extends EventEmitter {
return; return;
} }
} }
// console.log(mappings, this.hash);
} }
static get Node() { static get Node() {

163
upnp.js
View File

@ -1,163 +0,0 @@
/* node UPNP port forwarding PoC
This is a simple way to forward ports on NAT routers with UPNP.
This is a not-for-production hack that I found useful when testing apps
on my home network behind ny NAT router.
-satori / edited by smolleyes for freebox v6
usage: (install/clone node-ip from https://github.com/indutny/node-ip)
================================================================================
================================================================================
*/
var url = require("url");
var http = require("http");
var dgram = require("dgram");
var Buffer = require("buffer").Buffer;
// some const strings - dont change
const SSDP_PORT = 1901;
const bcast = "239.255.255.250";
const ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1";
const req = "M-SEARCH * HTTP/1.1\r\nHost:239.255.255.250:1900\r\n\
ST:"+ST+"\r\nMan:\"ssdp:discover\"\r\nMX:3\r\n\r\n";
const WANIP = "urn:schemas-upnp-org:service:WANIPConnection:1";
const OK = "HTTP/1.1 200 OK";
const SOAP_ENV_PRE = "<?xml version=\"1.0\"?>\n<s:Envelope \
xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" \
s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>\n";
const SOAP_ENV_POST = "</s:Body>\n</s:Envelope>\n";
function searchGateway(timeout, callback) {
var self = this;
var reqbuf = new Buffer(req, "ascii");
var socket = new dgram.Socket('udp4');
var clients = {};
var t;
if (timeout) {
t = setTimeout(function() {
onerror(new Error("searchGateway() timed out"));
}, timeout);
}
var onlistening = function() {
socket.setBroadcast(socket.fd, true);
// send a few packets just in case.
socket.send(reqbuf, 0, reqbuf.length, SSDP_PORT,bcast);
}
var onmessage = function(message, rinfo) {
console.log('message?')
message = message.toString();
if (message.substr(0, OK.length) !== OK ||
!message.indexOf(ST) ||
!message.indexOf("location:")) return;
var l = url.parse(message.match(/location:(.+?)\r\n/i)[1].trim());
if (clients[l.href]) return;
var client = clients[l.href] = http.createClient(l.port, l.hostname);
var request = client.request("GET", l.pathname, {"host": l.hostname});
request.end();
request.addListener('response', function (response) {
if (response.statusCode !== 200) return;
var resbuf = "";
response.addListener('data', function (chunk) { resbuf += chunk });
response.addListener("end", function() {
resbuf = resbuf.substr(resbuf.indexOf(WANIP) + WANIP.length);
var ipurl = resbuf.match(/<controlURL>(.+?)<\/controlURL>/i)[1].trim()
socket.close();
clearTimeout(t);
callback(null, new Gateway(l.port, l.hostname, ipurl));
});
});
}
var onerror = function(err) {
socket.close() ;
clearTimeout(t);
callback(err);
}
var onclose = function() {
socket.removeListener("listening", onlistening);
socket.removeListener("message", onmessage);
socket.removeListener("close", onclose);
socket.removeListener("error", onerror);
}
socket.addListener("listening", onlistening);
socket.addListener("message", onmessage);
socket.addListener("close", onclose);
socket.addListener("error", onerror);
socket.bind(SSDP_PORT);
}
exports.searchGateway = searchGateway;
function Gateway(port, host, path) {
this.port = port;
this.host = host;
this.path = path;
}
Gateway.prototype.getExternalIP = function(callback) {
var s =
"<u:GetExternalIPAddress xmlns:u=\"" + WANIP + "\">\
</u:GetExternalIPAddress>\n";
this._getSOAPResponse(s, "GetExternalIPAddress", function(err, xml) {
if (err) callback(err);
else callback(null,
xml.match(/<NewExternalIPAddress>(.+?)<\/NewExternalIPAddress>/i)[1]);
});
}
Gateway.prototype.AddPortMapping = function(protocol
, extPort
, intPort
, host
, description
, callback) {
var s =
"<u:AddPortMapping \
xmlns:u=\""+WANIP+"\">\
<NewRemoteHost></NewRemoteHost>\
<NewExternalPort>"+extPort+"</NewExternalPort>\
<NewProtocol>"+protocol+"</NewProtocol>\
<NewInternalPort>"+intPort+"</NewInternalPort>\
<NewInternalClient>"+host+"</NewInternalClient>\
<NewEnabled>1</NewEnabled>\
<NewPortMappingDescription>"+description+"</NewPortMappingDescription>\
<NewLeaseDuration>0</NewLeaseDuration>\
</u:AddPortMapping>";
this._getSOAPResponse(s, "AddPortMapping", callback);
}
Gateway.prototype._getSOAPResponse = function(soap, func, callback) {
var s = [SOAP_ENV_PRE, soap, SOAP_ENV_POST].join("");
var client = http.createClient(this.port, this.host);
var hdrs = { "host" : this.host
, "SOAPACTION" : "\"" + WANIP + "#" + func + "\""
, "content-type" : "text/xml"
, "content-length" : s.length };
var request = client.request("POST", this.path, hdrs);
request.end(s);
request.addListener('response', function (response) {
if (response.statusCode !== 200) {
response.close();
callback(new Error("Invalid SOAP action"));
return;
}
var buf = "";
response.addListener('data', function (chunk) { buf += chunk });
response.addListener('end', function () { callback(null, buf) });
});
}

View File

@ -2496,6 +2496,11 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
async@0.2.10:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
async@0.9.x: async@0.9.x:
version "0.9.2" version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@ -2688,6 +2693,13 @@ binary-extensions@^1.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
binary-search-tree@0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/binary-search-tree/-/binary-search-tree-0.2.5.tgz#7dbb3b210fdca082450dad2334c304af39bdc784"
integrity sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=
dependencies:
underscore "~1.4.4"
bindings@^1.5.0: bindings@^1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
@ -7572,6 +7584,13 @@ levn@~0.3.0:
prelude-ls "~1.1.2" prelude-ls "~1.1.2"
type-check "~0.3.2" type-check "~0.3.2"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
dependencies:
immediate "~3.0.5"
lie@~3.3.0: lie@~3.3.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
@ -7690,6 +7709,13 @@ loader-utils@^2.0.0:
emojis-list "^3.0.0" emojis-list "^3.0.0"
json5 "^2.1.2" json5 "^2.1.2"
localforage@^1.3.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
integrity sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==
dependencies:
lie "3.1.1"
locate-path@^2.0.0: locate-path@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@ -8249,6 +8275,17 @@ nearley@^2.7.10:
randexp "0.4.6" randexp "0.4.6"
semver "^5.4.1" semver "^5.4.1"
nedb@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/nedb/-/nedb-1.8.0.tgz#0e3502cd82c004d5355a43c9e55577bd7bd91d88"
integrity sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=
dependencies:
async "0.2.10"
binary-search-tree "0.2.5"
localforage "^1.3.0"
mkdirp "~0.5.1"
underscore "~1.4.4"
needle@^2.2.1: needle@^2.2.1:
version "2.5.2" version "2.5.2"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.2.tgz#cf1a8fce382b5a280108bba90a14993c00e4010a" resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.2.tgz#cf1a8fce382b5a280108bba90a14993c00e4010a"
@ -11546,6 +11583,11 @@ typical@^5.0.0, typical@^5.2.0:
resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
underscore@~1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
integrity sha1-YaajIBBiKvoHljvzJSA88SI51gQ=
unicode-canonical-property-names-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"