App supports controlling intval with bluetooth

This commit is contained in:
mmcwilliams 2017-12-13 17:52:25 -05:00
parent 52194e0b41
commit e5207a8474
11 changed files with 354 additions and 74 deletions

View File

@ -22,8 +22,10 @@
<allow-intent href="itms:*" />
<allow-intent href="itms-apps:*" />
</platform>
<preference name="DisallowOverscroll" value="true" />
<preference name="StatusBarBackgroundColor" value="#212121" />
<engine name="android" spec="^6.4.0" />
<engine name="ios" spec="^4.5.4" />
<engine name="android" spec="^6.3.0" />
<plugin name="cordova-plugin-whitelist" spec="^1.3.3" />
<plugin name="cordova-plugin-device" spec="^1.1.7" />
<plugin name="cordova-plugin-dialogs" spec="^1.3.4" />

15
app/package-lock.json generated
View File

@ -4,15 +4,10 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"android-versions": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/android-versions/-/android-versions-1.2.1.tgz",
"integrity": "sha512-k6zlrtWbJ3tx1ZsyyJ0Bo3r6cqPA3JUnFGv7pnIaLr1XVxSi2Tcem2lg3kBebFp27v/A40tZqdlouPyakpyKrw=="
},
"cordova-android": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/cordova-android/-/cordova-android-6.3.0.tgz",
"integrity": "sha1-2lQYQz0lx1pZd7QoJEu+Q30BKNI=",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/cordova-android/-/cordova-android-6.4.0.tgz",
"integrity": "sha1-VK6NpXKKjX5e/MYXLT3MoXvH/n0=",
"requires": {
"android-versions": "1.2.1",
"cordova-common": "2.1.0",
@ -27,6 +22,10 @@
"version": "1.1.0",
"bundled": true
},
"android-versions": {
"version": "1.2.1",
"bundled": true
},
"ansi": {
"version": "0.3.1",
"bundled": true

View File

@ -10,7 +10,7 @@
"author": "M McWilliams",
"license": "MIT",
"dependencies": {
"cordova-android": "^6.3.0",
"cordova-android": "^6.4.0",
"cordova-ios": "^4.5.4",
"cordova-plugin-ble-central": "^1.1.4",
"cordova-plugin-compat": "^1.2.0",
@ -30,8 +30,8 @@
}
},
"platforms": [
"ios",
"android"
"android",
"ios"
]
}
}

View File

@ -4,12 +4,15 @@
<title>INTVAL</title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width = 320, initial-scale = 1.0, user-scalable = no">
<meta name="viewport" content="width = 320, initial-scale = 1.0, user-scalable = no, minimal-ui, viewport-fit=cover">
<link rel="stylesheet" href="static/css/codemirror.css" />
<link rel="stylesheet" href="static/css/monokai.css" />
<link rel="stylesheet" href="static/css/index.css" />
</head>
<body>
<div id="overlay">
<div id="spinner"></div>
</div>
<div id="app" class="page selected">
<h2>INTVAL</h2>
<div>
@ -27,7 +30,7 @@
</div>
<div>
<div class="label">Exposure <span id="str">1/5</span></div>
<input type="number" id="exposure" value="630" min="0" oninput="setExposure();" />
<input type="number" id="exposure" value="630" min="0" onchange="setExposure();" />
<select id="scale" onchange="setExposureScale();">
<option value="ms" selected>ms</option>
<option value="sec">sec</option>
@ -37,7 +40,7 @@
</div>
<div>
<div class="label">Delay</div>
<input type="number" id="delay" value="0" min="0" step="1" />
<input type="number" id="delay" value="0" min="0" step="1" onchange="setDelay();" />
<select id="delayScale" onchange="setDelayScale();">
<option value="ms" selected>ms</option>
<option value="sec">sec</option>
@ -49,7 +52,7 @@
<button id="seq">START SEQUENCE</button>
</div>
<div>
<button id="frame" onclick="frame();">FRAME</button>
<button id="frame" onclick="frame();">1 FRAME</button>
</div>
</div>
<div id="settings" class="page">
@ -62,16 +65,20 @@
<option value="33">2 Stop</option>
</select>
</div>
<h2>BLUETOOTH</h2>
<select id="bluetooth">
<option>N/A</option>
</select>
<h2>WIFI</h2>
<div>
<input type="text" id="ssid" placeholder="Wifi SSID" />
<div class="ble">
<h2>BLUETOOTH</h2>
<select id="bluetooth">
<option>N/A</option>
</select>
</div>
<div>
<input type="password" id="password" placeholder="Wifi Password" />
<div class="ble">
<h2>WIFI</h2>
<div>
<input type="text" id="ssid" placeholder="Wifi SSID" />
</div>
<div>
<input type="password" id="password" placeholder="Wifi Password" />
</div>
</div>
</div>
<div id="mscript" class="page">
@ -92,6 +99,7 @@
</div>
</footer>
<script src="cordova.js"></script>
<script src="static/js/spin.min.js"></script>
<script src="static/js/codemirror.js"></script>
<script src="static/js/intval.core.js"></script>
<script src="static/js/intval.web.js"></script>

View File

@ -46,6 +46,12 @@ body{
.page.selected{
display: block;
}
.ble{
display: none;
}
.ble.active{
display: block;
}
h2{
font-size: 18px;
text-align: center;
@ -144,7 +150,8 @@ button{
padding: 5px 0;
text-align: center;
}
button:focus{
button:focus,
button.focus{
background-color: #20ce45;
border-color: #20ce45;
color: #212121;
@ -162,7 +169,7 @@ button:focus{
right: 10%;
}
.label{
text-align: center;
/*text-align: center;*/
color: #666;
margin-top: 6px;
margin-bottom: 9px;
@ -244,3 +251,26 @@ footer > div.selected{
#compile{
margin-top: 20px;
}
#seq{
margin-top: 40px;
}
#overlay{
position: fixed;
z-index: 2001;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
}
#overlay.active{
display: block;
}
#spinner{
margin-top: 200px;
}

View File

@ -35,7 +35,6 @@ var app = {
// 'pause', 'resume', etc.
onDeviceReady: function() {
mobile.init();
getState();
},
onDeviceResume : function () {
getState();

View File

@ -12,6 +12,7 @@ const STATE = {
delayScale : 'ms',
counter : 0
};
//functions
window.frame = null;
window.getState = null;
@ -93,7 +94,7 @@ var setExposureScale = function () {
};
var setDelayScale = function () {
const scale = document.getElementById('scale').value;
const scale = document.getElementById('delayScale').value;
const elem = document.getElementById('delay');
if (scale === 'ms') {
elem.value = STATE.delay;
@ -138,6 +139,36 @@ var unsetPages = function () {
}
};
var setState = function (res) {
let exposure;
let exposureScale;
let delayScale;
if (res.frame.dir !== true) {
document.getElementById('dir').checked = true;
STATE.dir = res.frame.dir;
setDirLabel(false);
}
document.getElementById('counter').value = res.counter;
STATE.counter = res.counter;
//Exposure
if (res.frame.exposure === 0) {
res.frame.exposure = BOLEX.expected;
}
STATE.exposure = res.frame.exposure;
exposure = shutter(STATE.exposure);
exposureScale = scaleAuto(STATE.exposure);
document.getElementById('str').value = exposure.str;
document.getElementById('scale').value = exposureScale;
setExposureScale();
STATE.delay = res.frame.delay;
delayScale = scaleAuto(STATE.delay);
document.getElementById('delayScale').value = delayScale;
setDelayScale();
};
var appPage = function () {
unsetPages();
document.getElementById('app').classList.add('selected');
@ -154,6 +185,44 @@ var mscriptPage = function () {
document.getElementById('mscriptIcon').classList.add('selected');
editor.cm.refresh();
};
var spinnerInit = function () {
const spinnerOpts = {
lines: 13 // The number of lines to draw
, length: 33 // The length of each line
, width: 11 // The line thickness
, radius: 30 // The radius of the inner circle
, scale: 0.5 // Scales overall size of the spinner
, corners: 1 // Corner roundness (0..1)
, color: '#fff' // #rgb or #rrggbb or array of colors
, opacity: 0.25 // Opacity of the lines
, rotate: 0 // The rotation offset
, direction: 1 // 1: clockwise, -1: counterclockwise
, speed: 1 // Rounds per second
, trail: 60 // Afterglow percentage
, fps: 20 // Frames per second when using setTimeout() as a fallback for CSS
, zIndex: 2e9 // The z-index (defaults to 2000000000)
, className: 'spinner' // The CSS class to assign to the spinner
, top: '50%' // Top position relative to parent
, left: '50%' // Left position relative to parent
, shadow: true // Whether to render a shadow
, hwaccel: true // Whether to use hardware acceleration
, position: 'relative' // Element positioning
};
const target = document.getElementById('spinner');
const spinner = new Spinner(spinnerOpts).spin(target);
};
var spinnerShow = function () {
const elem = document.getElementById('overlay');
if (!elem.classList.contains('active')) {
elem.classList.add('active');
}
};
var spinnerHide = function () {
const elem = document.getElementById('overlay');
if (elem.classList.contains('active')) {
elem.classList.remove('active');
}
}
var isNumeric = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};

View File

@ -7,27 +7,72 @@ mobile.ble = {
SERVICE_ID : '149582bd-d49d-4b5c-acd1-1ae503d09e7a',
CHAR_ID : '47bf69fb-f62f-4ef8-9be8-eb727a54fae4', //general data
WIFI_ID : '3fe7d9cf-7bd2-4ff0-97c5-ebe87288c2cc', //wifi only
DEVICES : []
devices : [],
device : {},
connected : false,
active : false
};
mobile.ble.scan = function () {
ble.scan([], 5, mobile.ble.onDiscover, BLE.onError);
spinnerShow();
ble.scan([], 5, mobile.ble.onDiscover, mobile.ble.onError);
mobile.ble.devices = [];
setTimeout(() => {
if (!mobile.ble.connected) {
ble.stopScan(() => {}, mobile.ble.onError);
spinnerHide();
alert('No INTVAL devices found.');
}
}, 5000)
};
mobile.ble.onDiscover = function (device) {
console.dir(device);
mobile.ble.connect(device.id);
if (device && device.name && device.name === 'intval3') {
console.log('BLE - Discovered INTVAL');
console.dir(device);
mobile.ble.devices.push(device);
if (!mobile.ble.connected) {
mobile.ble.connect(device);
}
} else {
//console.log(`BLE - Discovered Other ${device.id}`);
}
}
mobile.ble.connect = function (deviceId) {
ble.connect(deviceId, function (peripheral) {
mobile.ble.onConnect(peripheral, deviceId);
mobile.ble.connect = function (device) {
console.log(`BLE - Connecting to ${device.id}`)
ble.connect(device.id, (peripheral) => {
mobile.ble.onConnect(peripheral, device);
}, mobile.ble.onError);
};
mobile.ble.onConnect = function (peripheral, deviceId) {
mobile.ble.onConnect = function (peripheral, device) {
spinnerHide();
ble.stopScan(() => {}, moble.ble.onError);
console.log(`BLE - Connected to ${device.id}`);
console.log(peripheral);
console.log(deviceId);
console.dir(device);
mobile.ble.device = device;
mobile.ble.connected = true;
getState();
};
mobile.ble.disconnect = function () {
let device
if (!mobile.ble.connected) {
console.warn('Not connected to any device')
return false
}
device = mobile.ble.device
console.log(`BLE - Disconnecting from ${device.id}`)
ble.disconnect(device.id, mobile.ble.onDisconnect, mobile.ble.onDisconnect);
};
mobile.ble.onDisconnect = function (res) {
console.log(`BLE - Disconnected from ${res}`);
mobile.ble.connected = false;
mobile.ble.device = {};
};
mobile.ble.onError = function (err) {
@ -35,15 +80,154 @@ mobile.ble.onError = function (err) {
};
mobile.init = function () {
frame = mobile.frame;
getState = mobile.getState;
setDir = mobile.setDir;
setExposure = mobile.setExposure;
setCounter = mobile.setCounter;
const bleInputs = document.querySelectorAll('.ble')
window.frame = mobile.frame;
window.getState = mobile.getState;
window.setDir = mobile.setDir;
window.setExposure = mobile.setExposure;
window.setDelay = mobile.setDelay;
window.setCounter = mobile.setCounter;
//show ble-specific fields in settings
for (let i of bleInputs) {
i.classList.add('active');
}
spinnerInit();
mobile.ble.scan();
};
mobile.frame = function () {};
mobile.getState = function () {};
mobile.setDir = function () {};
mobile.setExposure = function () {};
mobile.setCounter = function () {};
mobile.getState = function () {
if (!mobile.ble.connected) {
//
}
ble.read(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
mobile.stateSuccess,
mobile.ble.onError);
};
mobile.stateSuccess = function (data) {
let str = bytesToString(data);
let res = JSON.parse(str);
setState(res)
};
mobile.frame = function () {
const opts = {
type : 'frame'
};
if (!mobile.ble.connected) {
return alert('Not connected to an INTVAL device.');
}
if (mobile.ble.active) {
return false;
}
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.frameSuccess,
mobile.ble.onError);
document.getElementById('frame').classList.add('focus');
mobile.ble.active = true;
};
mobile.frameSuccess = function () {
console.log('Frame finished, getting state.');
mobile.ble.active = false;
document.getElementById('frame').classList.remove('focus');
mobile.getState();
}
mobile.setDir = function () {
const opts = {
type : 'dir',
dir : !document.getElementById('dir').checked
};
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.dirSuccess,
mobile.ble.onError);
};
mobile.dirSuccess = function () {
console.log('Set direction');
mobile.getState();
};
mobile.setExposure = function () {
let exposure = document.getElementById('exposure').value;
let scaledExposure;
let opts = {
type : 'exposure'
};
if (exposure === '' || exposure === null) {
exposure = 0;
}
scaledExposure = scaleTime(exposure, STATE.scale);
opts.exposure = scaledExposure;
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.exposureSuccess,
mobile.ble.onError);
};
mobile.exposureSuccess = function () {
console.log('Set exposure');
mobile.getState();
};
mobile.setDelay = function () {
const delay = document.getElementById('delay').value;
const scaledDelay = scaleTime(delay, STATE.delayScale)
let opts = {
type : 'delay',
delay : scaledDelay
};
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.delaySuccess,
mobile.ble.onError);
}
mobile.delaySuccess = function () {
console.log('Set delay')
mobile.getState();
};
mobile.setCounter = function () {
const counter = document.getElementById('counter').value;
const change = prompt(`Change counter value?`, counter);
if (change === null || !isNumeric(change)) return false;
let opts = {
type : 'counter',
counter : change
};
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.counterSuccess,
mobile.ble.onError);
};
mobile.counterSuccess = function () {
console.log('Set counter');
mobile.getState();
};
function bytesToString (buffer) {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
};
function stringToBytes(string) {
var array = new Uint8Array(string.length);
for (var i = 0, l = string.length; i < l; i++) {
array[i] = string.charCodeAt(i);
}
return array.buffer;
};

View File

@ -55,36 +55,12 @@ web.getState = function () {
.then(res => {
return res.json();
})
.then(web.getStateSuccess)
.then(setState)
.catch(err => {
console.error('Error getting state');
console.error(err);
});
};
web.getStateSuccess = function (res) {
let exposure;
let scale;
if (res.frame.dir !== true) {
document.getElementById('dir').checked = true;
STATE.dir = res.frame.dir;
setDirLabel(false);
}
document.getElementById('counter').value = res.counter;
STATE.counter = res.counter;
//Exposure
if (res.frame.exposure === 0) {
res.frame.exposure = BOLEX.expected;
}
STATE.exposure = res.frame.exposure;
exposure = shutter(STATE.exposure);
scale = scaleAuto(STATE.exposure);
document.getElementById('str').value = exposure.str;
document.getElementById('scale').value = scale;
setExposureScale();
document.getElementById('delay').value = res.frame.delay;
STATE.delay = res.frame.delay;
};
web.setExposure = function () {
let exposure = document.getElementById('exposure').value;
let scaledExposure;
@ -165,6 +141,7 @@ web.init = function () {
window.frame = web.frame;
window.getState = web.getState;
window.setDir = web.setDir;
window.setDelay = web.setDelay;
window.setExposure = web.setExposure;
window.setCounter = web.setCounter;
console.log('started web')

2
app/www/static/js/spin.min.js vendored Normal file
View File

@ -0,0 +1,2 @@
// http://spin.js.org/#v2.3.2
!function(a,b){"object"==typeof module&&module.exports?module.exports=b():"function"==typeof define&&define.amd?define(b):a.Spinner=b()}(this,function(){"use strict";function a(a,b){var c,d=document.createElement(a||"div");for(c in b)d[c]=b[c];return d}function b(a){for(var b=1,c=arguments.length;c>b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return m[e]||(k.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",k.cssRules.length),m[e]=1),e}function d(a,b){var c,d,e=a.style;if(b=b.charAt(0).toUpperCase()+b.slice(1),void 0!==e[b])return b;for(d=0;d<l.length;d++)if(c=l[d]+b,void 0!==e[c])return c}function e(a,b){for(var c in b)a.style[d(a,c)||c]=b[c];return a}function f(a){for(var b=1;b<arguments.length;b++){var c=arguments[b];for(var d in c)void 0===a[d]&&(a[d]=c[d])}return a}function g(a,b){return"string"==typeof a?a:a[b%a.length]}function h(a){this.opts=f(a||{},h.defaults,n)}function i(){function c(b,c){return a("<"+b+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',c)}k.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.scale*d.width,left:d.scale*d.radius,top:-d.scale*d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.scale*(d.length+d.width),k=2*d.scale*j,l=-(d.width+d.length)*d.scale*2+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d<e.childNodes.length&&(e=e.childNodes[b+d],e=e&&e.firstChild,e=e&&e.firstChild,e&&(e.opacity=c))}}var j,k,l=["webkit","Moz","ms","O"],m={},n={lines:12,length:7,width:5,radius:10,scale:1,corners:1,color:"#000",opacity:.25,rotate:0,direction:1,speed:1,trail:100,fps:20,zIndex:2e9,className:"spinner",top:"50%",left:"50%",shadow:!1,hwaccel:!1,position:"absolute"};if(h.defaults={},f(h.prototype,{spin:function(b){this.stop();var c=this,d=c.opts,f=c.el=a(null,{className:d.className});if(e(f,{position:d.position,width:0,zIndex:d.zIndex,left:d.left,top:d.top}),b&&b.insertBefore(f,b.firstChild||null),f.setAttribute("role","progressbar"),c.lines(f,c.opts),!j){var g,h=0,i=(d.lines-1)*(1-d.direction)/2,k=d.fps,l=k/d.speed,m=(1-d.opacity)/(l*d.trail/100),n=l/d.lines;!function o(){h++;for(var a=0;a<d.lines;a++)g=Math.max(1-(h+(d.lines-a)*n)%l*m,d.opacity),c.opacity(f,a*d.direction+i,g,d);c.timeout=c.el&&setTimeout(o,~~(1e3/k))}()}return c},stop:function(){var a=this.el;return a&&(clearTimeout(this.timeout),a.parentNode&&a.parentNode.removeChild(a),this.el=void 0),this},lines:function(d,f){function h(b,c){return e(a(),{position:"absolute",width:f.scale*(f.length+f.width)+"px",height:f.scale*f.width+"px",background:b,boxShadow:c,transformOrigin:"left",transform:"rotate("+~~(360/f.lines*k+f.rotate)+"deg) translate("+f.scale*f.radius+"px,0)",borderRadius:(f.corners*f.scale*f.width>>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k<f.lines;k++)i=e(a(),{position:"absolute",top:1+~(f.scale*f.width/2)+"px",transform:f.hwaccel?"translate3d(0,0,0)":"",opacity:f.opacity,animation:j&&c(f.opacity,f.trail,l+k*f.direction,f.lines)+" "+1/f.speed+"s linear infinite"}),f.shadow&&b(i,e(h("#000","0 0 4px #000"),{top:"2px"})),b(d,b(i,h(g(f.color,k),"0 0 1px rgba(0,0,0,.1)")));return d},opacity:function(a,b,c){b<a.childNodes.length&&(a.childNodes[b].style.opacity=c)}}),"undefined"!=typeof document){k=function(){var c=a("style",{type:"text/css"});return b(document.getElementsByTagName("head")[0],c),c.sheet||c.styleSheet}();var o=e(a("group"),{behavior:"url(#default#VML)"});!d(o,"transform")&&o.adj?i():j=d(o,"animation")}return h});

12
package-lock.json generated
View File

@ -95,7 +95,8 @@
"integrity": "sha1-IesK10O850eU45L0ph4TsHOT26o=",
"requires": {
"bplist-parser": "0.0.6",
"debug": "2.6.8"
"debug": "2.6.8",
"xpc-connection": "0.1.4"
}
},
"bluebird": {
@ -2212,6 +2213,15 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xpc-connection": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/xpc-connection/-/xpc-connection-0.1.4.tgz",
"integrity": "sha1-3Nf6oq7Gt6bhjMXdrQQvejTHcVY=",
"optional": true,
"requires": {
"nan": "2.6.2"
}
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",