diff --git a/README.md b/README.md
index 4ddf53941..b8c93eb7a 100644
--- a/README.md
+++ b/README.md
@@ -49,3 +49,21 @@ Open browser at http://localhost:3000
```sh
yarn test
```
+
+## Logging
+
+Client-side logs are disabled by default in production builds and enabled by default in a development environment. In production, logging can be turned on by adding a couple keys to your browser's `localStorage`. Simply run these two JS statements in you browser's DevTools console:
+
+```
+localStorage.setItem('debug', '*'); localStorage.setItem('debug-level', 'debug');
+```
+
+The value for `debug` is a namespace filter which determines which portions of the app to display logs for. The namespaces currently used by the app are as follows:
+
+- `main`: logs general application messages
+- `action`: logs all actions that modify the internal application state
+- `grpc`: logs all GRPC API requests and responses
+
+Example filters: `main,action` will only log main and action messages. `*,-action` will log everything except action messages.
+
+The value for `debug-level` determines the verbosity of the logs. The value can be one of `debug`, `info`, `warn`, or `error`.
diff --git a/app/package.json b/app/package.json
index cbabe5091..d19cd430b 100644
--- a/app/package.json
+++ b/app/package.json
@@ -15,16 +15,21 @@
"proxy": "https://localhost:8443",
"dependencies": {
"@improbable-eng/grpc-web": "0.12.0",
+ "debug": "4.1.1",
+ "i18next": "19.4.1",
+ "i18next-browser-languagedetector": "4.0.2",
"mobx": "5.15.4",
"mobx-react": "6.2.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
+ "react-i18next": "11.3.4",
"react-scripts": "3.4.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
+ "@types/debug": "4.1.5",
"@types/google-protobuf": "3.7.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
@@ -48,6 +53,7 @@
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts",
"!src/types/**/*.{js,ts}",
+ "!src/i18n/**/*.{js,ts}",
"!src/index.tsx"
]
},
diff --git a/app/src/App.tsx b/app/src/App.tsx
index 41082072b..f787514a7 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -1,10 +1,12 @@
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import './App.css';
+import usePrefixedTranslation from 'hooks/usePrefixedTranslation';
import { channel, node, swap } from 'action';
import store from 'store';
const App = () => {
+ const { l } = usePrefixedTranslation('App');
useEffect(() => {
// fetch node info when the component is mounted
const fetchInfo = async () => await node.getInfo();
@@ -13,25 +15,25 @@ const App = () => {
return (
-
Node Info
+
{l('App.nodeInfo')}
{store.info && (
<>
- Pubkey |
+ {l('pubkey')} |
{store.info.identityPubkey} |
- Alias |
+ {l('alias')} |
{store.info.alias} |
- Version |
+ {l('version')} |
{store.info.version} |
- # Channels |
+ {l('numChannels')} |
{store.info.numActiveChannels} |
diff --git a/app/src/action/channel.ts b/app/src/action/channel.ts
index d3c683abe..cc7544a88 100644
--- a/app/src/action/channel.ts
+++ b/app/src/action/channel.ts
@@ -1,4 +1,5 @@
-import { action } from 'mobx';
+import { action, toJS } from 'mobx';
+import { actionLog as log } from 'util/log';
import LndApi from 'api/lnd';
import { Store } from 'store';
@@ -19,6 +20,7 @@ class ChannelAction {
* fetch channels from the LND RPC
*/
@action.bound async getChannels() {
+ log.info('fetching channels');
const channels = await this._lnd.listChannels();
this._store.channels = channels.channelsList.map(c => ({
chanId: c.chanId,
@@ -29,6 +31,7 @@ class ChannelAction {
uptime: c.uptime,
active: c.active,
}));
+ log.info('updated store.channels', toJS(this._store.channels));
}
}
diff --git a/app/src/action/node.ts b/app/src/action/node.ts
index 32f7f7069..bd8c4a2f1 100644
--- a/app/src/action/node.ts
+++ b/app/src/action/node.ts
@@ -1,4 +1,5 @@
-import { action } from 'mobx';
+import { action, toJS } from 'mobx';
+import { actionLog as log } from 'util/log';
import LndApi from 'api/lnd';
import { Store } from 'store';
@@ -19,7 +20,9 @@ class NodeAction {
* fetch node info from the LND RPC
*/
@action.bound async getInfo() {
+ log.info('fetching node information');
this._store.info = await this._lnd.getInfo();
+ log.info('updated store.info', toJS(this._store.info));
}
}
diff --git a/app/src/action/swap.ts b/app/src/action/swap.ts
index cb5470903..24ba6f71d 100644
--- a/app/src/action/swap.ts
+++ b/app/src/action/swap.ts
@@ -1,5 +1,6 @@
-import { action } from 'mobx';
+import { action, toJS } from 'mobx';
import { SwapState, SwapType } from 'types/generated/loop_pb';
+import { actionLog as log } from 'util/log';
import LoopApi from 'api/loop';
import { Store } from 'store';
@@ -20,6 +21,7 @@ class SwapAction {
* fetch swaps from the Loop RPC
*/
@action.bound async listSwaps() {
+ log.info('fetching Loop history');
const loopSwaps = await this._loop.listSwaps();
this._store.swaps = loopSwaps.swapsList
// sort the list with newest first as the API returns them out of order
@@ -31,6 +33,7 @@ class SwapAction {
createdOn: new Date(s.initiationTime / 1000 / 1000),
status: this._stateToString(s.state),
}));
+ log.info('updated store.swaps', toJS(this._store.swaps));
}
/**
diff --git a/app/src/api/grpc.ts b/app/src/api/grpc.ts
index 7d6f271f0..0ee8aab51 100644
--- a/app/src/api/grpc.ts
+++ b/app/src/api/grpc.ts
@@ -3,6 +3,7 @@ import { ProtobufMessage } from '@improbable-eng/grpc-web/dist/typings/message';
import { Metadata } from '@improbable-eng/grpc-web/dist/typings/metadata';
import { UnaryMethodDefinition } from '@improbable-eng/grpc-web/dist/typings/service';
import { DEV_HOST } from 'config';
+import { grpcLog as log } from 'util/log';
/**
* Executes a single GRPC request and returns a promise which will resolve with the response
@@ -16,16 +17,24 @@ export const grpcRequest = => {
return new Promise((resolve, reject) => {
+ log.debug(
+ `Request: ${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`,
+ );
+ log.debug(` - req: `, request.toObject());
grpc.unary(methodDescriptor, {
host: DEV_HOST,
request,
metadata,
onEnd: ({ status, statusMessage, headers, message, trailers }) => {
+ log.debug(' - status', status, statusMessage);
+ log.debug(' - headers', headers);
if (status === grpc.Code.OK && message) {
+ log.debug(' - message', message.toObject());
resolve(message as TRes);
} else {
reject(new Error(`${status}: ${statusMessage}`));
}
+ log.debug(' - trailers', trailers);
},
});
});
diff --git a/app/src/api/lnd.ts b/app/src/api/lnd.ts
index 1b78d3690..781dfc52f 100644
--- a/app/src/api/lnd.ts
+++ b/app/src/api/lnd.ts
@@ -16,7 +16,7 @@ class LndApi {
* call the LND `GetInfo` RPC and return the response
*/
async getInfo(): Promise {
- const req = new LND.GetInfoResponse();
+ const req = new LND.GetInfoRequest();
const res = await grpcRequest(Lightning.GetInfo, req, this._meta);
return res.toObject();
}
diff --git a/app/src/config.ts b/app/src/config.ts
index 3851e46cd..d4265f493 100644
--- a/app/src/config.ts
+++ b/app/src/config.ts
@@ -1,3 +1,9 @@
+// flag to check if the app is running in a local development environment
+export const IS_DEV = process.env.NODE_ENV === 'development';
+
+// flag to check if the app is running in a a production environment
+export const IS_PROD = process.env.NODE_ENV === 'production';
+
//
// temporary placeholder values. these will be supplied via the UI in the future
//
@@ -5,5 +11,8 @@
// macaroon to use for LND auth
export const DEV_MACAROON = process.env.REACT_APP_DEV_MACAROON || '';
+// detect the host currently serving the app files
+const { protocol, hostname, port = '' } = window.location;
+const host = `${protocol}//${hostname}:${port}`;
// the GRPC server to make requests to
-export const DEV_HOST = process.env.REACT_APP_DEV_HOST || 'http://localhost:3000';
+export const DEV_HOST = process.env.REACT_APP_DEV_HOST || host;
diff --git a/app/src/hooks/usePrefixedTranslation.ts b/app/src/hooks/usePrefixedTranslation.ts
new file mode 100644
index 000000000..45246514e
--- /dev/null
+++ b/app/src/hooks/usePrefixedTranslation.ts
@@ -0,0 +1,24 @@
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+
+/**
+ * A hook which returns a `t` function that inserts a prefix in each key lookup
+ * @param prefix the prefix to use for all translation keys
+ */
+const usePrefixedTranslation = (prefix: string) => {
+ const { t } = useTranslation();
+ // the new `t` function that will append the prefix
+ const translate = useCallback(
+ (key: string, options?: string | object) => {
+ // if the key contains a '.', then don't add the prefix
+ return key.includes('.') ? t(key, options) : t(`${prefix}.${key}`, options);
+ },
+ [prefix, t],
+ );
+
+ return {
+ l: translate,
+ };
+};
+
+export default usePrefixedTranslation;
diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts
new file mode 100644
index 000000000..1953f8f5f
--- /dev/null
+++ b/app/src/i18n/index.ts
@@ -0,0 +1,53 @@
+import { initReactI18next } from 'react-i18next';
+import i18n, { InitOptions } from 'i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import enUS from './locales/en-US.json';
+
+const defaultLanguage = 'en-US';
+
+export const languages: { [index: string]: string } = {
+ 'en-US': 'English',
+};
+
+/**
+ * create a mapping of locales -> translations
+ */
+const resources = Object.keys(languages).reduce((acc: { [key: string]: any }, lang) => {
+ switch (lang) {
+ case 'en-US':
+ acc[lang] = { translation: enUS };
+ break;
+ }
+ return acc;
+}, {});
+
+/**
+ * create an array of allowed languages
+ */
+const whitelist = Object.keys(languages).reduce((acc: string[], lang) => {
+ acc.push(lang);
+
+ if (lang.includes('-')) {
+ acc.push(lang.substring(0, lang.indexOf('-')));
+ }
+
+ return acc;
+}, []);
+
+const config: InitOptions = {
+ lng: defaultLanguage,
+ resources,
+ whitelist,
+ fallbackLng: defaultLanguage,
+ keySeparator: false,
+ interpolation: {
+ escapeValue: false,
+ },
+ detection: {
+ lookupLocalStorage: 'lang',
+ },
+};
+
+i18n.use(LanguageDetector).use(initReactI18next).init(config);
+
+export default i18n;
diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json
new file mode 100644
index 000000000..45085c924
--- /dev/null
+++ b/app/src/i18n/locales/en-US.json
@@ -0,0 +1,7 @@
+{
+ "App.nodeInfo": "Node Info",
+ "App.pubkey": "Pubkey",
+ "App.alias": "Alias",
+ "App.version": "Version",
+ "App.numChannels": "# Channels"
+}
diff --git a/app/src/index.tsx b/app/src/index.tsx
index 1f1b280de..bcab28470 100644
--- a/app/src/index.tsx
+++ b/app/src/index.tsx
@@ -1,9 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import 'mobx-react/batchingForReactDom';
+import './i18n';
import './index.css';
+import { log } from 'util/log';
import App from './App';
+log.info('Rendering the App');
+
ReactDOM.render(
diff --git a/app/src/setupTests.ts b/app/src/setupTests.ts
index bc6f73e8f..53363b55d 100644
--- a/app/src/setupTests.ts
+++ b/app/src/setupTests.ts
@@ -5,3 +5,5 @@ import 'mobx-react-lite/batchingForReactDom';
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
+// enable i18n translations in unit tests
+import './i18n';
diff --git a/app/src/util/log.ts b/app/src/util/log.ts
new file mode 100644
index 000000000..9b1a3f1e5
--- /dev/null
+++ b/app/src/util/log.ts
@@ -0,0 +1,99 @@
+import { IS_DEV } from 'config';
+import debug, { Debugger } from 'debug';
+
+enum LogLevel {
+ debug = 1,
+ info = 2,
+ warn = 3,
+ error = 4,
+ none = 5,
+}
+
+/**
+ * A logger class with support for multiple namespaces and log levels.
+ */
+class Logger {
+ private _levelToOutput: LogLevel;
+ private _logger: Debugger;
+
+ constructor(levelToOutput: LogLevel, namespace: string) {
+ this._levelToOutput = levelToOutput;
+ this._logger = debug(namespace);
+ }
+
+ /**
+ * creates a new Logger instance by inspecting the executing environment
+ */
+ static fromEnv(namespace: string): Logger {
+ // by default, log everything in development and nothing in production
+ let level = IS_DEV ? LogLevel.debug : LogLevel.none;
+
+ if (localStorage.getItem('debug')) {
+ // if a 'debug' key is found in localStorage, use the level in storage or 'debug' by default
+ const storageLevel = localStorage.getItem('debug-level') || 'debug';
+ level = LogLevel[storageLevel as keyof typeof LogLevel];
+ } else if (IS_DEV) {
+ // if running in development with no localStorage key, use debug
+ level = LogLevel.debug;
+ // set the keys so they can be easily changed in the browser DevTools
+ localStorage.setItem('debug', '*');
+ localStorage.setItem('debug-level', 'debug');
+ }
+
+ return new Logger(level, namespace);
+ }
+
+ /**
+ * log a debug message
+ */
+ debug = (message: string, ...args: any[]) => this._log(LogLevel.debug, message, args);
+
+ /**
+ * log an info message
+ */
+ info = (message: string, ...args: any[]) => this._log(LogLevel.info, message, args);
+
+ /**
+ * log a warn message
+ */
+ warn = (message: string, ...args: any[]) => this._log(LogLevel.warn, message, args);
+
+ /**
+ * log an error message
+ */
+ error = (message: string, ...args: any[]) => this._log(LogLevel.error, message, args);
+
+ /**
+ * A shared logging function which will only output logs based on the level of this Logger instance
+ * @param level the level of the message being logged
+ * @param message the message to log
+ * @param args optional additional arguments to log
+ */
+ private _log(level: LogLevel, message: string, args: any[]) {
+ // don't log if the level to output is greater than the level of this message
+ if (this._levelToOutput > level) return;
+
+ // convert the provided log level number to the string name
+ const prefix = Object.keys(LogLevel).reduce(
+ (prev, curr) => (level === LogLevel[curr as keyof typeof LogLevel] ? curr : prev),
+ '??',
+ );
+
+ this._logger(`[${prefix}] ${message}`, ...args);
+ }
+}
+
+/**
+ * the main logger for the app
+ */
+export const log = Logger.fromEnv('main');
+
+/**
+ * the logger for GRPC requests and responses
+ */
+export const grpcLog = Logger.fromEnv('grpc');
+
+/**
+ * the logger for state updates via mobx actions
+ */
+export const actionLog = Logger.fromEnv('action');
diff --git a/app/yarn.lock b/app/yarn.lock
index ed5be49ca..46082513a 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -899,7 +899,7 @@
dependencies:
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.9.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
@@ -1355,6 +1355,11 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+"@types/debug@4.1.5":
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
+ integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
+
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@@ -3463,6 +3468,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
+debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ dependencies:
+ ms "^2.1.1"
+
debug@^3.0.0, debug@^3.1.1, debug@^3.2.5:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@@ -3470,13 +3482,6 @@ debug@^3.0.0, debug@^3.1.1, debug@^3.2.5:
dependencies:
ms "^2.1.1"
-debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
- integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
- dependencies:
- ms "^2.1.1"
-
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -5003,6 +5008,13 @@ html-minifier-terser@^5.0.1:
relateurl "^0.2.7"
terser "^4.6.3"
+html-parse-stringify2@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
+ integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=
+ dependencies:
+ void-elements "^2.0.1"
+
html-webpack-plugin@4.0.0-beta.11:
version "4.0.0-beta.11"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.11.tgz#3059a69144b5aecef97708196ca32f9e68677715"
@@ -5102,6 +5114,20 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+i18next-browser-languagedetector@4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.0.2.tgz#eb02535cc5e57dd534fc60abeede05a3823a8551"
+ integrity sha512-AK4IZ3XST4HIKShgpB2gOFeDPrMOnZx56GLA6dGo/8rvkiczIlq05lV8w77c3ShEZxtTZeUVRI4Q/cBFFVXS/w==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+
+i18next@19.4.1:
+ version "19.4.1"
+ resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.4.1.tgz#4929d15d3d01e4712350a368d005cefa50ff5455"
+ integrity sha512-dC3ue15jkLebN2je4xEjfjVYd/fSAo+UVK9f+JxvceCJRowkI+S0lGohgKejqU+FYLfvw9IAPylIIEWwR8Djrg==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -8572,6 +8598,14 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
+react-i18next@11.3.4:
+ version "11.3.4"
+ resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.3.4.tgz#355df5fe5133e5e30302d166f529678100ffc968"
+ integrity sha512-IRZMD7PAM3C+fJNzRbyLNi1ZD0kc3Z3obBspJjEl+9H+ME41PhVor3BpdIqv/Rm7lUoGhMjmpu42J45ooJ61KA==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ html-parse-stringify2 "2.0.1"
+
react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -10353,6 +10387,11 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+void-elements@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+ integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+
w3c-hr-time@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"