Add redux, fix routing, add normalize

This commit is contained in:
Melvin Valster
2019-07-29 13:16:07 +02:00
parent 2fcf238446
commit 0bd3034ea7
13 changed files with 353 additions and 82 deletions
+1 -5
View File
@@ -1,12 +1,8 @@
# TODO
- [ ] Fix: Initial load `pointString` validation (make sure all talents are valid and their deps are met)
- [ ] Fix: Navigating between talent links for same class does not trigger re-render
- [ ] Fix: Tooltips cause horizontal scroll on less-wide screens. Investigate.
- [ ] Styling:
- [ ] SCSS: Normalize
- [ ] General:
- [ ] Add redux
- [ ] Responsiveness:
- [ ] Tooltips on mobile need different UX
- [ ] Talent tree:
+4
View File
@@ -14,8 +14,10 @@
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-loadable": "^5.5.0",
"react-redux": "^7.1.0",
"react-router-dom": "^5.0.1",
"react-scripts": "3.0.1",
"redux": "^4.0.4",
"typescript": "3.5.3"
},
"scripts": {
@@ -47,11 +49,13 @@
"@types/cheerio": "^0.22.12",
"@types/node-fetch": "^2.5.0",
"@types/react-loadable": "^5.5.1",
"@types/react-redux": "^7.1.1",
"@types/request": "^2.48.2",
"@welldone-software/why-did-you-render": "^3.2.1",
"cheerio": "^1.0.0-rc.3",
"gh-pages": "^2.0.1",
"node-fetch": "^2.6.0",
"redux-devtools": "^3.5.0",
"request": "^2.88.0",
"ts-node": "^8.3.0"
}
+1 -2
View File
@@ -1,5 +1,5 @@
import React from 'react'
import './App.scss'
import React from 'react'
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom'
import Loadable from 'react-loadable'
import { PageLoader } from './components/PageLoader'
@@ -17,7 +17,6 @@ const LoadablePlayground = Loadable({
const App: React.FC = () => {
return (
<Router>
{/* <Router basename={process.env.NODE_ENV !== 'development' ? '%PUBLIC_URL%' : ''}> */}
<div className="App">
<main>
<Switch>
+23 -46
View File
@@ -2,21 +2,18 @@ import './Calculator.scss'
import React from 'react'
import { Map } from 'immutable'
import { TalentTree } from './TalentTree'
import {
modifyTalentPoint,
calcAvailablePoints,
encodeKnownTalents,
} from '../lib/tree'
import { talentsBySpec } from '../data/talents'
import { classByName } from '../data/classes'
import { History } from 'history'
// import { debugPrintKnown } from '../lib/debug'
import { calcAvailablePoints } from '../lib/tree'
import { classById } from '../data/classes'
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { addPoint, removePoint } from '../store/calculator/actions'
import { Points } from '../store/calculator/types'
interface Props {
selectedClass: string
history: History
initialTalents?: Map<number, number>
classId: number
points: Points
addPoint: typeof addPoint
removePoint: typeof removePoint
}
const EMPTY_TALENTS = Map<number, number>()
@@ -28,46 +25,19 @@ export class Calculator extends React.PureComponent<Props> {
knownTalents: EMPTY_TALENTS
}
componentDidMount() {
if (this.props.initialTalents) {
this.setState({ knownTalents: this.props.initialTalents })
this.updateURL(this.props.initialTalents)
}
}
componentDidUpdate(prevProps: Props) {
if (prevProps.selectedClass !== this.props.selectedClass) {
this.setState({
knownTalents: EMPTY_TALENTS
})
}
}
updateURL(knownTalents: Map<number, number>) {
const { selectedClass } = this.props
const pointString = encodeKnownTalents(knownTalents, selectedClass)
this.props.history.replace(`/${selectedClass}` + (pointString ? `/${pointString}` : ''))
}
handleTalentPress = (specId: number, talentId: number, modifier: 1 | -1) => {
const talent = talentsBySpec[specId][talentId]
console.log('Clicked talent: ', talentId)
const newKnownTalents = modifyTalentPoint(this.state.knownTalents, talent, modifier)
if (newKnownTalents !== this.state.knownTalents) {
this.updateURL(newKnownTalents)
if (modifier === 1) {
this.props.addPoint(talentId)
} else {
this.props.removePoint(talentId)
}
this.setState({ knownTalents: newKnownTalents })
// Debug
// debugPrintKnown(newKnownTalents)
}
render() {
const { selectedClass } = this.props
const { knownTalents } = this.state
const { classId } = this.props
const knownTalents = this.props.points
const classData = classByName[selectedClass]
const classData = classById[classId]
const availablePoints = calcAvailablePoints(knownTalents)
return (
@@ -99,3 +69,10 @@ export class Calculator extends React.PureComponent<Props> {
}
}
export default connect(
null,
{
addPoint,
removePoint
}
)(Calculator)
+79 -15
View File
@@ -1,33 +1,90 @@
import React from 'react'
import { Calculator } from '../components/Calculator'
import { ClassPicker } from '../components/ClassPicker'
import { connect } from 'react-redux'
import { match } from 'react-router-dom'
import { RouteComponentProps } from 'react-router'
import { decodeKnownTalents } from '../lib/tree'
import { classByName } from '../data/classes'
import Calculator from '../components/Calculator'
import { ClassPicker } from '../components/ClassPicker'
import { classByName, classById } from '../data/classes'
import { AppState } from '../store'
import { setClass, setPoints } from '../store/calculator/actions'
import { Points } from '../store/calculator/types'
import { decodeKnownTalents, encodeKnownTalents } from '../lib/tree'
interface Props extends RouteComponentProps {
match: match<{
selectedClass: string
pointString: string
}>
classId: number
points: Points
setClass: typeof setClass
setPoints: typeof setPoints
}
export default class Home extends React.PureComponent<Props> {
export class Home extends React.PureComponent<Props> {
static whyDidYouRender = true
get classSlug() {
return classById[this.props.classId] && classById[this.props.classId].name.toLowerCase()
}
componentDidMount() {
const { selectedClass } = this.props.match.params
if (selectedClass && !classByName[selectedClass]) {
this.loadFromUrlParams()
}
componentDidUpdate(prevProps: Props) {
const prevParams = prevProps.match.params
const { params } = this.props.match
if (prevParams.selectedClass !== params.selectedClass) {
// Class changed in route
this.loadFromUrlParams()
} else {
// Changes within same class
if (prevParams.pointString !== params.pointString) {
// Same class but point string changed
const decoded = decodeKnownTalents(params.pointString || '', this.props.classId)
if (!this.props.points.equals(decoded)) {
this.props.setPoints(decoded)
}
} else if (prevProps.points !== this.props.points) {
// Points map changed, update the URL
this.updateURL(this.props.points)
}
}
}
componentWillUnmount() {
this.props.setClass(null)
}
loadFromUrlParams() {
const { selectedClass, pointString } = this.props.match.params
const c = selectedClass && classByName[selectedClass]
if (c) {
const points = pointString && decodeKnownTalents(pointString || '', c.id)
this.props.setClass(c.id, points)
} else {
this.props.setClass(null)
this.props.history.replace('/')
}
}
render() {
const { match, history } = this.props
const { selectedClass, pointString } = match.params
updateURL(points: Points) {
const { classId } = this.props
const pointsString = encodeKnownTalents(points, classId)
if (pointsString !== this.props.match.params.pointString) {
this.props.history.replace(`/${this.classSlug}` + (pointsString ? `/${pointsString}` : ''))
}
}
if (selectedClass && !classByName[selectedClass]) {
render() {
const { match, classId } = this.props
const { selectedClass } = match.params
const currentClass = classById[classId]
if (classId && !currentClass) {
// We're redirecting to /
return null
}
@@ -39,14 +96,21 @@ export default class Home extends React.PureComponent<Props> {
selected={selectedClass}
/>
{selectedClass &&
{currentClass &&
<Calculator
initialTalents={pointString && decodeKnownTalents(pointString, selectedClass)}
selectedClass={selectedClass}
history={history}
classId={classId}
points={this.props.points}
/>
}
</div>
)
}
}
export default connect(
({ calculator }: AppState) => ({
classId: calculator.classId,
points: calculator.points,
}),
{ setClass, setPoints }
)(Home)
+12 -2
View File
@@ -3,12 +3,22 @@ import ReactDOM from 'react-dom'
import App from './App'
import * as serviceWorker from './serviceWorker'
import { Provider } from 'react-redux'
import store from './store'
if (process.env.NODE_ENV !== 'production') {
const whyDidYouRender = require('@welldone-software/why-did-you-render/dist/no-classes-transpile/umd/whyDidYouRender.min.js')
whyDidYouRender(React)
whyDidYouRender(React, {
include: [/^ConnectFunction$/]
})
}
ReactDOM.render(<App />, document.getElementById('root'))
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
+5 -5
View File
@@ -5,7 +5,7 @@ import {
talentsBySpecArray,
talentsById
} from '../data/talents';
import { classByName } from '../data/classes'
import { classById } from '../data/classes'
import spells from '../data/spells.json'
export const MAX_POINTS = 51
@@ -204,9 +204,9 @@ export const modifyTalentPoint = (known: Map<number, number>, talent: TalentData
/**
* Encodes a Map of known talents into a URL-friendly string.
*/
export function encodeKnownTalents(known: Map<number, number>, className: string): string {
export function encodeKnownTalents(known: Map<number, number>, classId: number): string {
let string = ''
const { specs } = classByName[className]
const { specs } = classById[classId]
for (let i = 0; i < specs.length; i++) {
const specId = specs[i]
const talents = talentsBySpecArray[specId].sort(SORT_TALENTS)
@@ -222,8 +222,8 @@ export function encodeKnownTalents(known: Map<number, number>, className: string
/**
* Decodes a string of points into a Map of talents.
*/
export function decodeKnownTalents(pointString: string, className: string): Map<number, number> {
const { specs } = classByName[className]
export function decodeKnownTalents(pointString: string, classId: number): Map<number, number> {
const { specs } = classById[classId]
let known = Map<number, number>()
// TODO: Make sure we validate the point string
+29
View File
@@ -0,0 +1,29 @@
import {
CalculatorActionTypes,
SET_CLASS,
ADD_POINT,
REMOVE_POINT,
SET_POINTS,
Points,
} from './types'
export const setClass = (classId: number, points?: Points): CalculatorActionTypes => ({
type: SET_CLASS,
classId,
points
})
export const addPoint = (talentId: number): CalculatorActionTypes => ({
type: ADD_POINT,
talentId
})
export const removePoint = (talentId: number): CalculatorActionTypes => ({
type: REMOVE_POINT,
talentId
})
export const setPoints = (points: Points): CalculatorActionTypes => ({
type: SET_POINTS,
points
})
+76
View File
@@ -0,0 +1,76 @@
import { Map } from 'immutable'
import {
CalculatorState,
CalculatorActionTypes,
SET_CLASS,
ADD_POINT,
REMOVE_POINT,
SET_POINTS
} from './types'
import { canLearnTalent, canUnlearnTalent, encodeKnownTalents } from '../../lib/tree'
import { talentsById } from '../../data/talents'
const initialState: CalculatorState = {
classId: null,
points: Map<number, number>(),
pointsEncoded: ''
}
export default function(state = initialState, action: CalculatorActionTypes): CalculatorState {
const { classId, points } = state
switch (action.type) {
case SET_CLASS: {
if (classId === action.classId) {
return state
}
return {
...state,
classId: action.classId,
points: action.points || Map(),
pointsEncoded: ''
}
}
case ADD_POINT: {
const { talentId } = action
const talent = talentsById[talentId]
if (!canLearnTalent(points, talent)) {
return state
}
const nextPoints = points.set(talentId, points.get(talentId, 0) + 1)
return {
...state,
points: nextPoints,
pointsEncoded: encodeKnownTalents(nextPoints, classId)
}
}
case REMOVE_POINT: {
const { talentId } = action
const talent = talentsById[talentId]
if (!canUnlearnTalent(points, talent)) {
return state
}
const nextPoints = points.set(talentId, points.get(talentId, 1) - 1)
return {
...state,
points: nextPoints,
pointsEncoded: encodeKnownTalents(nextPoints, classId)
}
}
case SET_POINTS: {
if (points.equals(action.points)) {
return state
}
return {
...state,
points: action.points
}
}
default:
return state
}
}
+38
View File
@@ -0,0 +1,38 @@
import { Map } from 'immutable'
export type Points = Map<number, number>
export interface CalculatorState {
classId: number
points: Points
pointsEncoded: string
}
export const SET_CLASS = 'SET_CLASS'
export const ADD_POINT = 'ADD_POINT'
export const REMOVE_POINT = 'REMOVE_POINT'
export const SET_POINTS = 'SET_POINTS'
interface SetClassAction {
type: typeof SET_CLASS
classId: number
points?: Points
}
interface AddPointAction {
type: typeof ADD_POINT
talentId: number
}
interface RemovePointAction {
type: typeof REMOVE_POINT
talentId: number
}
interface SetPointsAction {
type: typeof SET_POINTS
points: Points
}
export type CalculatorActionTypes = SetClassAction | AddPointAction | RemovePointAction |
SetPointsAction
+17
View File
@@ -0,0 +1,17 @@
import { createStore, combineReducers, compose } from 'redux'
import calculator from './calculator/reducers'
const rootReducer = combineReducers({
calculator,
})
export type AppState = ReturnType<typeof rootReducer>
const store = createStore(
rootReducer,
compose(
(window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__()
),
)
export default store
+1
View File
@@ -52,3 +52,4 @@ interface Talent {
type TalentClickHandler = (specId: number, talentId: number, modifier: 1 | -1) => void
type TooltipPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'left' | 'right'
+65 -5
View File
@@ -881,7 +881,7 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
@@ -1293,6 +1293,14 @@
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220"
integrity sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q==
"@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"
@@ -1367,6 +1375,16 @@
"@types/react" "*"
"@types/webpack" "*"
"@types/react-redux@^7.1.1":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.1.tgz#eb01e89cf71cad77df9f442b819d5db692b997cb"
integrity sha512-owqNahzE8en/jR4NtrUJDJya3tKru7CIEGSRL/pVS84LtSCdSoT7qZTkrbBd3S4Lp11sAp+7LsvxIeONJVKMnw==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@^4.3.4":
version "4.3.4"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.4.tgz#63a7a8558129d2f4ff76e4bdd099bf4b98e25a0d"
@@ -4745,7 +4763,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==
@@ -6273,7 +6291,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, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.5, lodash@~4.17.10:
"lodash@>=3.5 <5", lodash@^4, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.5, lodash@^4.2.0, lodash@~4.17.10:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -8216,7 +8234,7 @@ prompts@^2.0.1:
kleur "^3.0.2"
sisteransi "^1.0.0"
prop-types@^15.5.0, prop-types@^15.6.2:
prop-types@^15.5.0, prop-types@^15.5.7, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -8443,7 +8461,7 @@ react-error-overlay@^5.1.6:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.6.tgz#0cd73407c5d141f9638ae1e0c63e7b2bf7e9929d"
integrity sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q==
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4:
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
@@ -8455,6 +8473,18 @@ react-loadable@^5.5.0:
dependencies:
prop-types "^15.5.0"
react-redux@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2"
integrity sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==
dependencies:
"@babel/runtime" "^7.4.5"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.8.6"
react-router-dom@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be"
@@ -8658,6 +8688,31 @@ redent@^1.0.0:
indent-string "^2.1.0"
strip-indent "^1.0.1"
redux-devtools-instrument@^1.9.0:
version "1.9.6"
resolved "https://registry.yarnpkg.com/redux-devtools-instrument/-/redux-devtools-instrument-1.9.6.tgz#6b412595f74b9d48cfd4ecc13e585b1588ed6e7e"
integrity sha512-MwvY4cLEB2tIfWWBzrUR02UM9qRG2i7daNzywRvabOSVdvAY7s9BxSwMmVRH1Y/7QWjplNtOwgT0apKhHg2Qew==
dependencies:
lodash "^4.2.0"
symbol-observable "^1.0.2"
redux-devtools@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/redux-devtools/-/redux-devtools-3.5.0.tgz#d69ab76d4f0f8abdf6d24bcf5954d7a1aa2b6827"
integrity sha512-pGU8TZNvWxPaCCE432AGm6H6alQbAz80gQM5CzM3SjX9/oSNu/HPF17xFdPQJOXasqyih1Gv167kZDTRe7r0iQ==
dependencies:
lodash "^4.2.0"
prop-types "^15.5.7"
redux-devtools-instrument "^1.9.0"
redux@^4.0.0, redux@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796"
integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
regenerate-unicode-properties@^8.0.2:
version "8.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
@@ -9694,6 +9749,11 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
symbol-observable@^1.0.2, 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"