implement bluetooth connect using redux

This commit is contained in:
David Lechner
2020-04-16 19:09:50 -05:00
parent 0a50055574
commit b4d81fd3e5
11 changed files with 368 additions and 131 deletions
+7 -1
View File
@@ -10,6 +10,9 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.7",
"@types/redux-logger": "^3.0.7",
"@types/web-bluetooth": "^0.0.5",
"ace-builds": "^1.4.9",
"bootstrap": "^4.4.1",
"bootswatch": "^4.4.1",
@@ -19,7 +22,11 @@
"react-bootstrap": "^1.0.0",
"react-dom": "^16.13.1",
"react-dropzone": "^10.2.2",
"react-redux": "^7.2.0",
"react-scripts": "3.4.1",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"typescript": "~3.7.2",
"xterm": "^4.5.0"
},
@@ -47,7 +54,6 @@
]
},
"devDependencies": {
"@types/web-bluetooth": "^0.0.5",
"@typescript-eslint/eslint-plugin": "^2.27.0",
"@typescript-eslint/parser": "^2.27.0",
"eslint": "^6.8.0",
+119
View File
@@ -0,0 +1,119 @@
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
const pybricksServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef';
const bleNusServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const bleNusCharRXUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
const bleNusCharTXUUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
let device: BluetoothDevice | undefined;
let rxChar: BluetoothRemoteGATTCharacteristic | undefined;
export enum BLEConnectActionType {
/**
* Begin async connect.
*/
BeginConnect = 'ble.connect.begin',
/**
* End async connect (success).
*/
EndConnect = 'ble.connect.end',
/**
* Begin async disconnect.
*/
BeginDisconnect = 'ble.disconnect.begin',
/**
* End async disconnect (can be sent without sending BeginDisconnect first).
*/
EndDisconnect = 'ble.disconnect.end',
}
type BLEConnectAction = Action<BLEConnectActionType>;
enum BLEDataActionType {
/**
* Send data.
*/
SendData = 'ble.data.send',
/**
* Data was received.
*/
ReceivedData = 'ble.data.receive',
}
interface BLEDataAction extends Action<BLEDataActionType> {
value: DataView;
}
type AnyBLEAction = BLEConnectAction | BLEDataAction;
type BLEThunkAction = ThunkAction<Promise<void>, {}, {}, AnyBLEAction>;
function beginConnect(): BLEConnectAction {
return { type: BLEConnectActionType.BeginConnect };
}
function endConnect(): BLEConnectAction {
return { type: BLEConnectActionType.EndConnect };
}
function beginDisconnect(): BLEConnectAction {
return { type: BLEConnectActionType.BeginDisconnect };
}
function endDisconnect(): BLEConnectAction {
return { type: BLEConnectActionType.EndDisconnect };
}
export function connect(): BLEThunkAction {
return async function (dispatch): Promise<void> {
if (device !== undefined) {
console.error('Already have a connected device');
return;
}
if (navigator.bluetooth === undefined) {
// TODO: dispatch error toast action
console.error('Browser does not support WebBluetooth or it is not enabled');
return;
}
dispatch(beginConnect());
device = await navigator.bluetooth.requestDevice({
filters: [{ services: [pybricksServiceUUID] }],
optionalServices: [bleNusServiceUUID],
});
if (device.gatt === undefined) {
console.error('Device does not support GATT');
return;
}
device.addEventListener('gattserverdisconnected', () => {
device = undefined;
rxChar = undefined;
dispatch(endDisconnect());
});
const server = await device.gatt.connect();
try {
const service = await server.getPrimaryService(bleNusServiceUUID);
rxChar = await service.getCharacteristic(bleNusCharRXUUID);
const txChar = await service.getCharacteristic(bleNusCharTXUUID);
txChar.addEventListener('characteristicvaluechanged', () => {
if (!txChar.value) {
return;
}
dispatch({ type: BLEDataActionType.ReceivedData, value: txChar.value });
});
await txChar.startNotifications();
} catch (err) {
console.error('getting nRF UART service failed');
device.gatt.disconnect();
return;
}
dispatch(endConnect());
};
}
export function disconnect(): BLEThunkAction {
return async function (dispatch): Promise<void> {
dispatch(beginDisconnect());
device?.gatt?.disconnect();
};
}
+16 -12
View File
@@ -4,31 +4,35 @@ import Tooltip from 'react-bootstrap/Tooltip';
import Image from 'react-bootstrap/Image';
import React from 'react';
interface ActionButtonProperties {
interface ActionButtonProps {
/** A unique id for each instance. */
readonly id: string;
readonly action: {
readonly id: string;
readonly tooltip: string;
readonly icon: string;
};
readonly onAction: (action: string) => void;
/** Tooltip text that appears when hovering over the button. */
readonly tooltip: string;
/** Icon shown on the button. */
readonly icon: string;
/** When true or undefined, the button is enabled. */
readonly enabled?: boolean;
/** Optional action hint passed to the onAction() callback. */
readonly action?: string;
/** Callback that is called when the button is activated (clicked). */
readonly onAction: (action?: string) => void;
}
class ActionButton extends React.Component<ActionButtonProperties> {
class ActionButton extends React.Component<ActionButtonProps> {
render(): JSX.Element {
return (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id={`${this.props.id}-tooltip`}>
{this.props.action.tooltip}.
{this.props.tooltip}.
</Tooltip>
}
>
<Button
variant="primary"
onClick={(): void => this.props.onAction(this.props.action.id)}
onClick={(): void => this.props.onAction(this.props.action)}
disabled={this.props.enabled === false}
style={
this.props.enabled === false
@@ -37,8 +41,8 @@ class ActionButton extends React.Component<ActionButtonProperties> {
}
>
<Image
src={`/static/images/${this.props.action.icon}`}
alt={this.props.action.id}
src={`/static/images/${this.props.icon}`}
alt={this.props.id}
/>
</Button>
</OverlayTrigger>
+50
View File
@@ -0,0 +1,50 @@
import ActionButton from './ActionButton';
import { connect } from 'react-redux';
import { connect as bleConnect, disconnect as bleDisconnect } from '../actions/ble';
import { RootState } from '../reducers';
import { BLEConnectionState } from '../reducers/ble';
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
interface StateProps {
readonly tooltip: string;
readonly icon: string;
readonly action?: string;
readonly enabled?: boolean;
}
interface DispatchProps {
readonly onAction: (action?: string) => void;
}
const mapStateToProps = (state: RootState): StateProps => {
if (state.ble.connection === BLEConnectionState.Disconnected) {
return {
tooltip: 'Connect using Bluetooth',
icon: 'btdisconnected.svg',
action: 'connect',
enabled: true,
};
} else {
return {
tooltip: 'Disconnect Bluetooth',
icon: 'btconnected.svg',
action: 'disconnect',
enabled: state.ble.connection === BLEConnectionState.Connected,
};
}
};
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onAction: (a): void => {
if (a === 'connect') {
dispatch(bleConnect());
} else {
dispatch(bleDisconnect());
}
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ActionButton);
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import ProgressBar from 'react-bootstrap/ProgressBar';
import { BLEConnectionState } from '../services/BLEConnection';
import { BLEConnectionState } from '../reducers/ble';
interface StatusBarState {
bleState: BLEConnectionState;
+12 -78
View File
@@ -4,19 +4,13 @@ import ButtonGroup from 'react-bootstrap/ButtonGroup';
import ButtonToolbar from 'react-bootstrap/ButtonToolbar';
import React from 'react';
import ActionButton from './ActionButton';
import { BLEConnectionState } from '../services/BLEConnection';
import { BLEConnectionState } from '../reducers/ble';
import BluetoothButton from './BluetoothButton';
interface ToolbarState {
bleState: BLEConnectionState;
}
function oneOf(
bleState: BLEConnectionState,
...matches: BLEConnectionState[]
): boolean {
return matches.indexOf(bleState) >= 0;
}
class Toolbar extends React.Component<{}, ToolbarState> {
constructor(props: {}) {
super(props);
@@ -24,7 +18,7 @@ class Toolbar extends React.Component<{}, ToolbarState> {
this.onAction = this.onAction.bind(this);
}
private onAction(action: string): void {
private onAction(action?: string): void {
console.log(action);
}
@@ -38,92 +32,32 @@ class Toolbar extends React.Component<{}, ToolbarState> {
<Col>
<ButtonToolbar className="m-2">
<ButtonGroup className="mr-2" size="lg">
<ActionButton
id="bluetooth"
action={
this.state.bleState ===
BLEConnectionState.Disconnected
? {
id: 'connect',
tooltip: 'Connect using Bluetooth',
icon: 'btdisconnected.svg',
}
: {
id: 'disconnect',
tooltip: 'Disconnect Bluetooth',
icon: 'btconnected.svg',
}
}
onAction={this.onAction}
/>
<BluetoothButton id="bluetooth" />
<ActionButton
id="run"
action={{
id: 'run',
tooltip: 'Download and run this program',
icon: 'run.svg',
}}
tooltip="Download and run this program"
icon="run.svg"
onAction={this.onAction}
enabled={oneOf(
this.state.bleState,
BLEConnectionState.Waiting,
BLEConnectionState.Running,
BLEConnectionState.REPL,
)}
/>
<ActionButton
id="stop"
action={
this.state.bleState ===
BLEConnectionState.Downloading
? {
id: 'cancel',
tooltip: 'Cancel download',
icon: 'stop.svg',
}
: {
id: 'reset',
tooltip: 'Stop everything',
icon: 'stop.svg',
}
}
tooltip="Stop everything"
icon="stop.svg"
onAction={this.onAction}
enabled={oneOf(
this.state.bleState,
BLEConnectionState.Waiting,
BLEConnectionState.Downloading,
BLEConnectionState.Running,
BLEConnectionState.REPL,
)}
/>
</ButtonGroup>
<ButtonGroup className="mr-2" size="lg">
<ActionButton
id="repl"
action={{
id: 'repl',
tooltip: 'Start REPL in terminal',
icon: 'repl.svg',
}}
tooltip="Start REPL in terminal"
icon="repl.svg"
onAction={this.onAction}
enabled={oneOf(
this.state.bleState,
BLEConnectionState.Waiting,
BLEConnectionState.REPL,
)}
/>
<ActionButton
id="flash"
action={{
id: 'flash',
tooltip: 'Flash hub firmware',
icon: 'firmware.svg',
}}
tooltip="Flash hub firmware"
icon="firmware.svg"
onAction={this.onAction}
enabled={oneOf(
this.state.bleState,
BLEConnectionState.Disconnected,
)}
/>
</ButtonGroup>
</ButtonToolbar>
+15 -1
View File
@@ -1,12 +1,26 @@
import thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger';
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import React from 'react';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import './index.scss';
import App from './components/App';
import * as serviceWorker from './serviceWorker';
const loggerMiddleware = createLogger();
const store = createStore(
rootReducer,
applyMiddleware(thunkMiddleware, loggerMiddleware),
);
ReactDOM.render(
<React.StrictMode>
<App />
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root'),
);
+51
View File
@@ -0,0 +1,51 @@
import { Reducer, combineReducers } from 'redux';
import { BLEConnectActionType } from '../actions/ble';
/**
* Describes the state of the BLE connection.
*/
export enum BLEConnectionState {
/**
* No device is connected.
*/
Disconnected = 'ble.connection.disconnected',
/**
* Connecting to a device.
*/
Connecting = 'ble.connection.connecting',
/**
* Connected to a device.
*/
Connected = 'ble.connection.connected',
/**
* Disconnecting from a device.
*/
Disconnecting = 'ble.connection.disconnecting',
}
/**
* BLE state for redux store.
*/
export interface BLEState {
readonly connection: BLEConnectionState;
}
const connection: Reducer<BLEConnectionState> = (
state = BLEConnectionState.Disconnected,
action,
) => {
switch (action.type) {
case BLEConnectActionType.BeginConnect:
return BLEConnectionState.Connecting;
case BLEConnectActionType.EndConnect:
return BLEConnectionState.Connected;
case BLEConnectActionType.BeginDisconnect:
return BLEConnectionState.Disconnecting;
case BLEConnectActionType.EndDisconnect:
return BLEConnectionState.Disconnected;
default:
return state;
}
};
export default combineReducers({ connection });
+11
View File
@@ -0,0 +1,11 @@
import { combineReducers } from 'redux';
import ble, { BLEState } from './ble';
/**
* Root state for redux store.
*/
export interface RootState {
readonly ble: BLEState;
}
export default combineReducers({ ble });
-35
View File
@@ -1,35 +0,0 @@
/**
* Describes the state of the BLE connection.
*/
enum BLEConnectionState {
/**
* No device is connected.
*/
Disconnected,
/**
* Scanning for devices.
*/
Scanning,
/**
* Connecting to a device.
*/
Connecting,
/**
* Connected, waiting for a command.
*/
Waiting,
/**
* Connected, busy downloading a user program.
*/
Downloading,
/**
* Connected, running a user program.
*/
Running,
/**
* Connected, running REPL.
*/
REPL,
}
export { BLEConnectionState };
+86 -3
View File
@@ -2096,6 +2096,14 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -2165,6 +2173,16 @@
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.7":
version "7.1.7"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.7.tgz#12a0c529aba660696947384a059c5c6e08185c7a"
integrity sha512-U+WrzeFfI83+evZE2dkZ/oF/1vjIYgqrb5dGgedkqVV8HEfDFujNgWCwHL89TDuWKb47U0nTBT6PLGq4IIogWg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react@*", "@types/react@^16.9.0":
version "16.9.32"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.32.tgz#f6368625b224604148d1ddf5920e4fefbd98d383"
@@ -2181,6 +2199,13 @@
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/redux-logger@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.7.tgz#163f6f6865c69c21d56f9356dc8d741718ec0db0"
integrity sha512-oV9qiCuowhVR/ehqUobWWkXJjohontbDGLV88Be/7T4bqMQ3kjXwkFNL7doIIqlbg3X2PC5WPziZ8/j/QHNQ4A==
dependencies:
redux "^3.6.0"
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -4309,6 +4334,11 @@ decode-uri-component@^0.2.0:
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
deep-diff@^0.3.5:
version "0.3.8"
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84"
integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=
deep-equal@^1.0.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
@@ -5958,6 +5988,13 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4:
version "2.8.5"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
@@ -7520,7 +7557,7 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.15:
lodash-es@^4.17.15, lodash-es@^4.2.1:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
@@ -7570,7 +7607,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5, lodash@~4.17.12:
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.12:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -9975,7 +10012,7 @@ 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-is@^16.12.0, react-is@^16.3.2:
react-is@^16.12.0, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -10003,6 +10040,17 @@ react-overlays@^3.0.1:
uncontrollable "^7.0.0"
warning "^4.0.3"
react-redux@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
dependencies:
"@babel/runtime" "^7.5.5"
hoist-non-react-statics "^3.3.0"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.9.0"
react-scripts@3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.4.1.tgz#f551298b5c71985cc491b9acf3c8e8c0ae3ada0a"
@@ -10201,6 +10249,36 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
redux-logger@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"
integrity sha1-91VZZvMJjzyIYExEnPC69XeCdL8=
dependencies:
deep-diff "^0.3.5"
redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
redux@^3.6.0:
version "3.7.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==
dependencies:
lodash "^4.2.1"
lodash-es "^4.2.1"
loose-envify "^1.1.0"
symbol-observable "^1.0.3"
redux@^4.0.0, redux@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
regenerate-unicode-properties@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
@@ -11470,6 +11548,11 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
symbol-observable@^1.0.3, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
symbol-tree@^3.2.2:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"