Cleanup socketio stuff for typescript

This commit is contained in:
Dane Everitt 2018-12-16 18:57:34 -08:00
parent 3ad4422a94
commit 5e4ca8ef83
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
22 changed files with 246 additions and 210 deletions

View File

@ -20,7 +20,9 @@
"@babel/plugin-transform-async-to-generator": "^7.0.0-beta.49", "@babel/plugin-transform-async-to-generator": "^7.0.0-beta.49",
"@babel/plugin-transform-runtime": "^7.0.0-beta.49", "@babel/plugin-transform-runtime": "^7.0.0-beta.49",
"@babel/preset-env": "^7.0.0-beta.49", "@babel/preset-env": "^7.0.0-beta.49",
"@types/lodash": "^4.14.119",
"@types/node": "^10.12.15", "@types/node": "^10.12.15",
"@types/socket.io-client": "^1.4.32",
"@types/webpack-env": "^1.13.6", "@types/webpack-env": "^1.13.6",
"autoprefixer": "^8.2.0", "autoprefixer": "^8.2.0",
"axios": "^0.18.0", "axios": "^0.18.0",
@ -33,7 +35,6 @@
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-plugin-transform-strict-mode": "^6.18.0", "babel-plugin-transform-strict-mode": "^6.18.0",
"babel-register": "^6.26.0", "babel-register": "^6.26.0",
"camelcase": "^5.0.0",
"clean-webpack-plugin": "^0.1.19", "clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"eslint": "^5.6.0", "eslint": "^5.6.0",

View File

@ -69,7 +69,7 @@
import ProgressBar from './components/ProgressBar'; import ProgressBar from './components/ProgressBar';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import io from 'socket.io-client'; import io from 'socket.io-client';
import { Socketio } from './../../mixins/socketio'; import { Socketio } from '../../mixins/socketio/index';
import PowerButtons from './components/PowerButtons'; import PowerButtons from './components/PowerButtons';
import Flash from '../Flash'; import Flash from '../Flash';

View File

@ -24,7 +24,7 @@
<script> <script>
import Status from '../../../helpers/statuses'; import Status from '../../../helpers/statuses';
import { Socketio } from './../../../mixins/socketio'; import { Socketio } from '../../../mixins/socketio/index';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {

View File

@ -15,7 +15,7 @@
</template> </template>
<script> <script>
import * as Helpers from './../../../../helpers/index'; import * as Helpers from '../../../../helpers/index';
import { FileTextIcon, Link2Icon } from 'vue-feather-icons'; import { FileTextIcon, Link2Icon } from 'vue-feather-icons';
import FileManagerContextMenu from './FileManagerContextMenu'; import FileManagerContextMenu from './FileManagerContextMenu';

View File

@ -16,7 +16,7 @@
<script> <script>
import { FolderIcon } from 'vue-feather-icons'; import { FolderIcon } from 'vue-feather-icons';
import { formatDate } from './../../../../helpers/index'; import { formatDate } from '../../../../helpers/index';
export default { export default {
name: 'file-manager-folder-row', name: 'file-manager-folder-row',

View File

@ -28,7 +28,7 @@
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import * as TerminalFit from 'xterm/lib/addons/fit/fit'; import * as TerminalFit from 'xterm/lib/addons/fit/fit';
import {mapState} from 'vuex'; import {mapState} from 'vuex';
import {Socketio} from './../../../mixins/socketio'; import {Socketio} from '../../../mixins/socketio/index';
Terminal.applyAddon(TerminalFit); Terminal.applyAddon(TerminalFit);

View File

@ -1,15 +1,18 @@
import axios, {AxiosResponse} from 'axios';
/** /**
* We'll load the axios HTTP library which allows us to easily issue requests * We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the * to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie. * CSRF token as a header based on the value of the "XSRF" token cookie.
*/ */
let axios = require('axios');
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.headers.common['Accept'] = 'application/json'; axios.defaults.headers.common['Accept'] = 'application/json';
// Attach the response data to phpdebugbar so that we can see everything happening.
// @ts-ignore
if (typeof phpdebugbar !== 'undefined') { if (typeof phpdebugbar !== 'undefined') {
axios.interceptors.response.use(function (response) { axios.interceptors.response.use(function (response: AxiosResponse) {
// @ts-ignore
phpdebugbar.ajaxHandler.handle(response.request); phpdebugbar.ajaxHandler.handle(response.request);
return response; return response;

View File

@ -1,19 +1,16 @@
import format from 'date-fns/format'; import { format } from 'date-fns';
/** /**
* Return the human readable filesize for a given number of bytes. This * Return the human readable filesize for a given number of bytes. This
* uses 1024 as the base, so the response is denoted accordingly. * uses 1024 as the base, so the response is denoted accordingly.
*
* @param {Number} bytes
* @return {String}
*/ */
export function readableSize (bytes) { export function readableSize (bytes: number): string {
if (Math.abs(bytes) < 1024) { if (Math.abs(bytes) < 1024) {
return `${bytes} Bytes`; return `${bytes} Bytes`;
} }
let u = -1; let u: number = -1;
const units = ['KiB', 'MiB', 'GiB', 'TiB']; const units: Array<string> = ['KiB', 'MiB', 'GiB', 'TiB'];
do { do {
bytes /= 1024; bytes /= 1024;
@ -25,10 +22,7 @@ export function readableSize (bytes) {
/** /**
* Format the given date as a human readable string. * Format the given date as a human readable string.
*
* @param {String} date
* @return {String}
*/ */
export function formatDate (date) { export function formatDate (date: string): string {
return format(date, 'MMM D, YYYY [at] HH:MM'); return format(date, 'MMM D, YYYY [at] HH:MM');
} }

View File

@ -2,60 +2,50 @@ export const flash = {
methods: { methods: {
/** /**
* Flash a message to the event stream in the browser. * Flash a message to the event stream in the browser.
*
* @param {string} message
* @param {string} title
* @param {string} severity
*/ */
flash: function (message, title, severity = 'info') { flash: function (message: string, title: string, severity: string = 'info'): void {
severity = severity || 'info'; severity = severity || 'info';
if (['danger', 'fatal', 'error'].includes(severity)) { if (['danger', 'fatal', 'error'].includes(severity)) {
severity = 'error'; severity = 'error';
} }
// @ts-ignore
window.events.$emit('flash', { message, title, severity }); window.events.$emit('flash', { message, title, severity });
}, },
/** /**
* Clear all of the flash messages from the screen. * Clear all of the flash messages from the screen.
*/ */
clearFlashes: function () { clearFlashes: function (): void {
// @ts-ignore
window.events.$emit('clear-flashes'); window.events.$emit('clear-flashes');
}, },
/** /**
* Helper function to flash a normal success message to the user. * Helper function to flash a normal success message to the user.
*
* @param {string} message
*/ */
success: function (message) { success: function (message: string): void {
this.flash(message, 'Success', 'success'); this.flash(message, 'Success', 'success');
}, },
/** /**
* Helper function to flash a normal info message to the user. * Helper function to flash a normal info message to the user.
*
* @param {string} message
*/ */
info: function (message) { info: function (message: string): void {
this.flash(message, 'Info', 'info'); this.flash(message, 'Info', 'info');
}, },
/** /**
* Helper function to flash a normal warning message to the user. * Helper function to flash a normal warning message to the user.
*
* @param {string} message
*/ */
warning: function (message) { warning: function (message: string): void {
this.flash(message, 'Warning', 'warning'); this.flash(message, 'Warning', 'warning');
}, },
/** /**
* Helper function to flash a normal error message to the user. * Helper function to flash a normal error message to the user.
*
* @param {string} message
*/ */
error: function (message) { error: function (message: string): void {
this.flash(message, 'Error', 'danger'); this.flash(message, 'Error', 'danger');
}, },
} }

View File

@ -1,103 +0,0 @@
import io from 'socket.io-client';
import camelCase from 'camelcase';
import SocketEmitter from './emitter';
const SYSTEM_EVENTS = [
'connect',
'error',
'disconnect',
'reconnect',
'reconnect_attempt',
'reconnecting',
'reconnect_error',
'reconnect_failed',
'connect_error',
'connect_timeout',
'connecting',
'ping',
'pong',
];
export default class SocketioConnector {
constructor (store = null) {
this.socket = null;
this.store = store;
}
/**
* Initialize a new Socket connection.
*
* @param {io} socket
*/
connect (socket) {
if (!socket instanceof io) {
throw new Error('First argument passed to connect() should be an instance of socket.io-client.');
}
this.socket = socket;
this.registerEventListeners();
}
/**
* Return the socket instance we are working with.
*
* @return {io|null}
*/
instance () {
return this.socket;
}
/**
* Register the event listeners for this socket including user-defined ones in the store as
* well as global system events from Socekt.io.
*/
registerEventListeners () {
this.socket['onevent'] = (packet) => {
const [event, ...args] = packet.data;
SocketEmitter.emit(event, ...args);
this.passToStore(event, args);
};
SYSTEM_EVENTS.forEach((event) => {
this.socket.on(event, (payload) => {
SocketEmitter.emit(event, payload);
this.passToStore(event, payload);
})
});
}
/**
* Pass event calls off to the Vuex store if there is a corresponding function.
*
* @param {String|Number|Symbol} event
* @param {Array} payload
*/
passToStore (event, payload) {
if (!this.store) {
return;
}
const mutation = `SOCKET_${event.toUpperCase()}`;
const action = `socket_${camelCase(event)}`;
Object.keys(this.store._mutations).filter((namespaced) => {
return namespaced.split('/').pop() === mutation;
}).forEach((namespaced) => {
this.store.commit(namespaced, this.unwrap(payload));
});
Object.keys(this.store._actions).filter((namespaced) => {
return namespaced.split('/').pop() === action;
}).forEach((namespaced) => {
this.store.dispatch(namespaced, this.unwrap(payload));
});
}
/**
* @param {Array} args
* @return {Array<Object>|Object}
*/
unwrap (args) {
return (args && args.length <= 1) ? args[0] : args;
}
}

View File

@ -0,0 +1,115 @@
import * as io from 'socket.io-client';
import {camelCase} from 'lodash';
import SocketEmitter from './emitter';
import {Store} from "vuex";
const SYSTEM_EVENTS: Array<string> = [
'connect',
'error',
'disconnect',
'reconnect',
'reconnect_attempt',
'reconnecting',
'reconnect_error',
'reconnect_failed',
'connect_error',
'connect_timeout',
'connecting',
'ping',
'pong',
];
export default class SocketioConnector {
/**
* The socket instance.
*/
socket: null | SocketIOClient.Socket;
/**
* The vuex store being used to persist data and socket state.
*/
store: Store<any> | undefined;
constructor(store: Store<any> | undefined) {
this.socket = null;
this.store = store;
}
/**
* Initialize a new Socket connection.
*
* @param {io} socket
*/
connect(socket: SocketIOClient.Socket) {
this.socket = socket;
this.registerEventListeners();
}
/**
* Return the socket instance we are working with.
*/
instance(): SocketIOClient.Socket | null {
return this.socket;
}
/**
* Register the event listeners for this socket including user-defined ones in the store as
* well as global system events from Socekt.io.
*/
registerEventListeners() {
if (!this.socket) {
return;
}
// @ts-ignore
this.socket['onevent'] = (packet: { data: Array<any> }): void => {
const [event, ...args] = packet.data;
SocketEmitter.emit(event, ...args);
this.passToStore(event, args);
};
SYSTEM_EVENTS.forEach((event: string): void => {
if (!this.socket) {
return;
}
this.socket.on(event, (payload: any) => {
SocketEmitter.emit(event, payload);
this.passToStore(event, payload);
});
});
}
/**
* Pass event calls off to the Vuex store if there is a corresponding function.
*/
passToStore(event: string | number, payload: Array<any>) {
if (!this.store) {
return;
}
const s: Store<any> = this.store;
const mutation = `SOCKET_${String(event).toUpperCase()}`;
const action = `socket_${camelCase(String(event))}`;
// @ts-ignore
Object.keys(this.store._mutations).filter((namespaced: string): boolean => {
return namespaced.split('/').pop() === mutation;
}).forEach((namespaced: string): void => {
s.commit(namespaced, this.unwrap(payload));
});
// @ts-ignore
Object.keys(this.store._actions).filter((namespaced: string): boolean => {
return namespaced.split('/').pop() === action;
}).forEach((namespaced: string): void => {
s.dispatch(namespaced, this.unwrap(payload)).catch(console.error);
});
}
unwrap(args: Array<any>) {
return (args && args.length <= 1) ? args[0] : args;
}
}

View File

@ -1,18 +1,21 @@
import isFunction from 'lodash/isFunction'; import {isFunction} from 'lodash';
import {ComponentOptions} from "vue";
import {Vue} from "vue/types/vue";
export default new class SocketEmitter { export default new class SocketEmitter {
constructor () { listeners: Map<string | number, Array<{
callback: (a: ComponentOptions<Vue>) => void,
vm: ComponentOptions<Vue>,
}>>;
constructor() {
this.listeners = new Map(); this.listeners = new Map();
} }
/** /**
* Add an event listener for socket events. * Add an event listener for socket events.
*
* @param {String|Number|Symbol} event
* @param {Function} callback
* @param {*} vm
*/ */
addListener (event, callback, vm) { addListener(event: string | number, callback: (data: any) => void, vm: ComponentOptions<Vue>) {
if (!isFunction(callback)) { if (!isFunction(callback)) {
return; return;
} }
@ -21,21 +24,19 @@ export default new class SocketEmitter {
this.listeners.set(event, []); this.listeners.set(event, []);
} }
// @ts-ignore
this.listeners.get(event).push({callback, vm}); this.listeners.get(event).push({callback, vm});
} }
/** /**
* Remove an event listener for socket events based on the context passed through. * Remove an event listener for socket events based on the context passed through.
*
* @param {String|Number|Symbol} event
* @param {Function} callback
* @param {*} vm
*/ */
removeListener (event, callback, vm) { removeListener(event: string | number, callback: (data: any) => void, vm: ComponentOptions<Vue>) {
if (!isFunction(callback) || !this.listeners.has(event)) { if (!isFunction(callback) || !this.listeners.has(event)) {
return; return;
} }
// @ts-ignore
const filtered = this.listeners.get(event).filter((listener) => { const filtered = this.listeners.get(event).filter((listener) => {
return listener.callback !== callback || listener.vm !== vm; return listener.callback !== callback || listener.vm !== vm;
}); });
@ -49,13 +50,10 @@ export default new class SocketEmitter {
/** /**
* Emit a socket event. * Emit a socket event.
*
* @param {String|Number|Symbol} event
* @param {Array} args
*/ */
emit (event, ...args) { emit(event: string | number, ...args: any) {
(this.listeners.get(event) || []).forEach((listener) => { (this.listeners.get(event) || []).forEach((listener) => {
listener.callback.call(listener.vm, ...args); listener.callback.call(listener.vm, args);
}); });
} }
} }

View File

@ -1,9 +1,11 @@
import SocketEmitter from './emitter'; import SocketEmitter from './emitter';
import SocketioConnector from './connector'; import SocketioConnector from './connector';
import {ComponentOptions} from 'vue';
import {Vue} from "vue/types/vue";
let connector = null; let connector: SocketioConnector | null = null;
export const Socketio = { export const Socketio: ComponentOptions<Vue> = {
/** /**
* Setup the socket when we create the first component using the mixin. This is the Server.vue * Setup the socket when we create the first component using the mixin. This is the Server.vue
* file, unless you mess up all of this code. Subsequent components to use this mixin will * file, unless you mess up all of this code. Subsequent components to use this mixin will
@ -15,7 +17,7 @@ export const Socketio = {
connector = new SocketioConnector(this.$store); connector = new SocketioConnector(this.$store);
} }
const sockets = this.$options.sockets || {}; const sockets = (this.$options || {}).sockets || {};
Object.keys(sockets).forEach((event) => { Object.keys(sockets).forEach((event) => {
SocketEmitter.addListener(event, sockets[event], this); SocketEmitter.addListener(event, sockets[event], this);
}); });
@ -25,7 +27,7 @@ export const Socketio = {
* Before destroying the component we need to remove any event listeners registered for it. * Before destroying the component we need to remove any event listeners registered for it.
*/ */
beforeDestroy: function () { beforeDestroy: function () {
const sockets = this.$options.sockets || {}; const sockets = (this.$options || {}).sockets || {};
Object.keys(sockets).forEach((event) => { Object.keys(sockets).forEach((event) => {
SocketEmitter.removeListener(event, sockets[event], this); SocketEmitter.removeListener(event, sockets[event], this);
}); });
@ -43,8 +45,13 @@ export const Socketio = {
* Disconnects from the active socket and sets the connector to null. * Disconnects from the active socket and sets the connector to null.
*/ */
removeSocket: function () { removeSocket: function () {
if (connector !== null && connector.instance() !== null) { if (!connector) {
connector.instance().close(); return;
}
const instance: SocketIOClient.Socket | null = connector.instance();
if (instance) {
instance.close();
} }
connector = null; connector = null;

View File

@ -1,20 +1,14 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import auth, {AuthenticationState} from './modules/auth'; import auth from './modules/auth';
import dashboard, {DashboardState} from './modules/dashboard'; import dashboard from './modules/dashboard';
import server, {ServerState} from './modules/server'; import server from './modules/server';
import socket, {SocketState} from './modules/socket'; import socket from './modules/socket';
import {ApplicationState} from "./types";
Vue.use(Vuex); Vue.use(Vuex);
export type ApplicationState = { const store = new Vuex.Store<ApplicationState>({
socket: SocketState,
server: ServerState,
auth: AuthenticationState,
dashboard: DashboardState,
}
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production', strict: process.env.NODE_ENV !== 'production',
modules: {auth, dashboard, server, socket}, modules: {auth, dashboard, server, socket},
}); });

View File

@ -1,24 +1,19 @@
import User, {UserData} from '../../models/user'; import User, {UserData} from '../../models/user';
import {ActionContext} from "vuex"; import {ActionContext} from "vuex";
import {AuthenticationState} from "../types";
const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').default; const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').default;
type LoginAction = { type LoginAction = {
type: 'login',
user: string, user: string,
password: string, password: string,
} }
type UpdateEmailAction = { type UpdateEmailAction = {
type: 'updateEmail',
email: string, email: string,
password: string, password: string,
} }
export type AuthenticationState = {
user: null | User,
}
export default { export default {
namespaced: true, namespaced: true,
state: { state: {

View File

@ -1,12 +1,8 @@
import Server, {ServerData} from '../../models/server'; import Server, {ServerData} from '../../models/server';
import {ActionContext} from "vuex"; import {ActionContext} from "vuex";
import {DashboardState} from "../types";
const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').default; const route = require('./../../../../../vendor/tightenco/ziggy/src/js/route').default;
export type DashboardState = {
searchTerm: string,
servers: Array<Server>,
};
export default { export default {
namespaced: true, namespaced: true,
state: { state: {

View File

@ -2,17 +2,7 @@
import route from '../../../../../vendor/tightenco/ziggy/src/js/route'; import route from '../../../../../vendor/tightenco/ziggy/src/js/route';
import {ActionContext} from "vuex"; import {ActionContext} from "vuex";
import {ServerData} from "../../models/server"; import {ServerData} from "../../models/server";
import {ServerApplicationCredentials, ServerState} from "../types";
type ServerApplicationCredentials = {
node: string,
key: string,
};
export type ServerState = {
server: ServerData,
credentials: ServerApplicationCredentials,
console: Array<string>,
};
export default { export default {
namespaced: true, namespaced: true,

View File

@ -1,10 +1,5 @@
import Status from '../../helpers/statuses'; import Status from '../../helpers/statuses';
import {SocketState} from "../types";
export type SocketState = {
connected: boolean,
connectionError: boolean | Error,
status: number,
}
export default { export default {
namespaced: true, namespaced: true,

View File

@ -0,0 +1,37 @@
import {ServerData} from "../models/server";
import Server from "../models/server";
import User from "../models/user";
export type ApplicationState = {
socket: SocketState,
server: ServerState,
auth: AuthenticationState,
dashboard: DashboardState,
}
export type SocketState = {
connected: boolean,
connectionError: boolean | Error,
status: number,
}
export type ServerApplicationCredentials = {
node: string,
key: string,
};
export type ServerState = {
server: ServerData,
credentials: ServerApplicationCredentials,
console: Array<string>,
};
export type DashboardState = {
searchTerm: string,
servers: Array<Server>,
};
export type AuthenticationState = {
user: null | User,
}

View File

@ -1,4 +1,23 @@
import Vue, {ComponentOptions} from "vue";
import {Store} from "vuex";
declare module '*.vue' { declare module '*.vue' {
import Vue from 'vue';
export default Vue; export default Vue;
} }
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
$store?: Store<any>,
$options?: {
sockets?: {
[s: string]: (data: any) => void,
}
},
}
}
declare module 'vue/types/vue' {
interface Vue {
$store: Store<any>,
}
}

View File

@ -1,8 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es6",
"lib": [ "lib": [
"es2015", "es2015",
"es2016",
"dom" "dom"
], ],
"strict": true, "strict": true,

View File

@ -778,10 +778,18 @@
"@shellscape/koa-send" "^4.1.0" "@shellscape/koa-send" "^4.1.0"
debug "^2.6.8" debug "^2.6.8"
"@types/lodash@^4.14.119":
version "4.14.119"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39"
"@types/node@^10.12.15": "@types/node@^10.12.15":
version "10.12.15" version "10.12.15"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.15.tgz#20e85651b62fd86656e57c9c9bc771ab1570bc59" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.15.tgz#20e85651b62fd86656e57c9c9bc771ab1570bc59"
"@types/socket.io-client@^1.4.32":
version "1.4.32"
resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.32.tgz#988a65a0386c274b1c22a55377fab6a30789ac14"
"@types/webpack-env@^1.13.6": "@types/webpack-env@^1.13.6":
version "1.13.6" version "1.13.6"
resolved "http://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.13.6.tgz#128d1685a7c34d31ed17010fc87d6a12c1de6976" resolved "http://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.13.6.tgz#128d1685a7c34d31ed17010fc87d6a12c1de6976"
@ -1725,10 +1733,6 @@ camelcase@^4.0.0, camelcase@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
camelcase@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
caniuse-api@^1.5.2: caniuse-api@^1.5.2:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c"