From cd48874d612e3f4af07eae9422a25ea31de92edf Mon Sep 17 00:00:00 2001 From: Shivam Kantival Date: Sun, 29 Mar 2020 02:00:43 +0530 Subject: [PATCH] add platform pusher notification service --- package.json | 3 + src/App/index.tsx | 12 +-- .../{interfaces.ts => interfaces/actions.ts} | 0 src/config/interfaces/index.ts | 2 + src/config/interfaces/notifications.ts | 11 +++ .../components/NotificationCard/index.tsx | 17 +++++ .../components/NotificationCard/styles.ts | 24 ++++++ .../components/Notifications/index.tsx | 35 +++++++++ .../components/Notifications/style.ts | 74 +++++++++++++++++++ .../PlatformPusherNotifications/helpers.ts | 18 +++++ .../PlatformPusherNotifications/index.tsx | 73 ++++++++++++++++++ .../PlatformPusherNotifications/interfaces.ts | 14 ++++ .../components/UsersListView/index.tsx | 2 +- src/containers/UsersList/helpers.ts | 15 ++++ src/containers/UsersList/index.tsx | 42 ++++++++--- src/containers/UsersList/thunks.ts | 2 - src/context/PlatformPusherNotification.ts | 13 ++++ src/styles/mixins/index.ts | 8 ++ src/styles/theme.ts | 4 + src/utils/index.ts | 1 + src/utils/pausableTimer.ts | 43 +++++++++++ yarn.lock | 34 ++++++++- 22 files changed, 427 insertions(+), 20 deletions(-) rename src/config/{interfaces.ts => interfaces/actions.ts} (100%) create mode 100644 src/config/interfaces/index.ts create mode 100644 src/config/interfaces/notifications.ts create mode 100644 src/containers/PlatformPusherNotifications/components/NotificationCard/index.tsx create mode 100644 src/containers/PlatformPusherNotifications/components/NotificationCard/styles.ts create mode 100644 src/containers/PlatformPusherNotifications/components/Notifications/index.tsx create mode 100644 src/containers/PlatformPusherNotifications/components/Notifications/style.ts create mode 100644 src/containers/PlatformPusherNotifications/helpers.ts create mode 100644 src/containers/PlatformPusherNotifications/index.tsx create mode 100644 src/containers/PlatformPusherNotifications/interfaces.ts create mode 100644 src/containers/UsersList/helpers.ts create mode 100644 src/context/PlatformPusherNotification.ts create mode 100644 src/utils/pausableTimer.ts diff --git a/package.json b/package.json index 7befd23..078e687 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,15 @@ "@types/node": "^12.0.0", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", + "@types/react-transition-group": "^4.2.4", "@types/styled-components": "^5.0.1", "lodash": "^4.17.15", "query-string": "^6.11.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-scripts": "3.4.1", + "react-transition-group": "^4.3.0", + "reselect": "^4.0.0", "styled-components": "^5.0.1", "typescript": "~3.7.2" }, diff --git a/src/App/index.tsx b/src/App/index.tsx index d0d654d..f9ea4ac 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -4,14 +4,16 @@ import React from 'react'; import Header from 'components/Header'; import StyledAppContainer from './styles'; import AppContent from 'components/AppContent'; +import PlatformPusherNotifications from 'containers/PlatformPusherNotifications'; function App() { return ( - -
- - + + +
+ + + ); } - export default App; diff --git a/src/config/interfaces.ts b/src/config/interfaces/actions.ts similarity index 100% rename from src/config/interfaces.ts rename to src/config/interfaces/actions.ts diff --git a/src/config/interfaces/index.ts b/src/config/interfaces/index.ts new file mode 100644 index 0000000..97917a5 --- /dev/null +++ b/src/config/interfaces/index.ts @@ -0,0 +1,2 @@ +export * from './actions'; +export * from './notifications'; diff --git a/src/config/interfaces/notifications.ts b/src/config/interfaces/notifications.ts new file mode 100644 index 0000000..e421e23 --- /dev/null +++ b/src/config/interfaces/notifications.ts @@ -0,0 +1,11 @@ +export enum NOTIFICATION_TYPES { + SUCCESS, + ERROR, + INFO, +} + +export type NewNotificationDetails = { + readonly timeout?: number; + readonly type: NOTIFICATION_TYPES; + readonly message: string; +}; diff --git a/src/containers/PlatformPusherNotifications/components/NotificationCard/index.tsx b/src/containers/PlatformPusherNotifications/components/NotificationCard/index.tsx new file mode 100644 index 0000000..762d224 --- /dev/null +++ b/src/containers/PlatformPusherNotifications/components/NotificationCard/index.tsx @@ -0,0 +1,17 @@ +import React, { memo } from 'react'; + +//typeDefs +import { Notification } from '../../interfaces'; + +//components +import StyledNotificationCard from './styles'; + +type NotificationCardProps = { + notification: Notification; +}; + +const NotificationCard: React.FC = ({ notification }) => ( + {notification.message} +); + +export default memo(NotificationCard); diff --git a/src/containers/PlatformPusherNotifications/components/NotificationCard/styles.ts b/src/containers/PlatformPusherNotifications/components/NotificationCard/styles.ts new file mode 100644 index 0000000..8503ad1 --- /dev/null +++ b/src/containers/PlatformPusherNotifications/components/NotificationCard/styles.ts @@ -0,0 +1,24 @@ +import styled from 'styled-components'; +import { COLORS, spacing } from 'styles/theme'; +import { NOTIFICATION_TYPES } from 'config/interfaces'; +import { flexContainer } from 'styles/mixins'; + +const NOTIFICATION_TYPE_TO_BACKGROUND_COLOR = { + [NOTIFICATION_TYPES.SUCCESS]: COLORS.PASTEL_GREEN, + [NOTIFICATION_TYPES.ERROR]: COLORS.BITTERSWEET, + [NOTIFICATION_TYPES.INFO]: COLORS.HONEY, +}; + +type StyledNotificationCardProps = { + type: NOTIFICATION_TYPES; +}; + +export default styled.span` + height: 100%; + margin-bottom: ${spacing(2)}; + border-radius: ${spacing(1)}; + padding: ${spacing(2)}; + background: ${(props: StyledNotificationCardProps) => + NOTIFICATION_TYPE_TO_BACKGROUND_COLOR[props.type]}; + ${flexContainer({ alignItems: 'center', justifyContent: 'flex-end' })} +`; diff --git a/src/containers/PlatformPusherNotifications/components/Notifications/index.tsx b/src/containers/PlatformPusherNotifications/components/Notifications/index.tsx new file mode 100644 index 0000000..5483614 --- /dev/null +++ b/src/containers/PlatformPusherNotifications/components/Notifications/index.tsx @@ -0,0 +1,35 @@ +import React, { memo, ReactNode } from 'react'; + +//typeDefs +import { Notification } from '../../interfaces'; + +//components +import StyledNotificationsView from './style'; +import NotificationCard from '../NotificationCard'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; + +//utils +import map from 'lodash/map'; + +type NotificationsProps = { + notifications: ReadonlyArray; +}; + +const Notifications: React.FC = ({ notifications }) => { + return ( + + + {map( + notifications, + (notif: Notification): ReactNode => ( + + + + ) + )} + + + ); +}; + +export default memo(Notifications); diff --git a/src/containers/PlatformPusherNotifications/components/Notifications/style.ts b/src/containers/PlatformPusherNotifications/components/Notifications/style.ts new file mode 100644 index 0000000..acd2fc9 --- /dev/null +++ b/src/containers/PlatformPusherNotifications/components/Notifications/style.ts @@ -0,0 +1,74 @@ +import styled from 'styled-components'; +import { spacing } from 'styles/theme'; +import { flexContainer } from 'styles/mixins'; + +const CARD_HEIGHT = spacing(10); + +export default styled.div` + .notificationListContainer { + ${flexContainer({ flexDirection: 'column', alignItems: 'flex-end' })} + } + .animation-enter-done { + height: ${CARD_HEIGHT}; + } + + .animation-enter { + opacity: 1; + height: ${CARD_HEIGHT}; + } + + .animation-enter-active { + animation: enter 500ms ease-out; + position: relative; + + @keyframes enter { + from { + opacity: 0; + height: 0; + right: -${spacing(5)}; + } + 75% { + right: ${spacing(4)}; + } + to { + opacity: 1; + height: ${CARD_HEIGHT}; + right: 0; + } + } + } + + .animation-exit-done { + height: 0; + } + + .animation-exit { + opacity: 0; + height: ${CARD_HEIGHT}; + } + + .animation-exit-active { + animation: exit 500ms ease-out; + position: relative; + + @keyframes exit { + from { + opacity: 1; + height: ${CARD_HEIGHT}; + right: 0; + } + 25% { + right: ${spacing(4)}; + } + to { + opacity: 0; + height: 0; + right: -${spacing(8)}; + } + } + } + + position: fixed; + bottom: ${spacing(10)}; + right: ${spacing(5)}; +`; diff --git a/src/containers/PlatformPusherNotifications/helpers.ts b/src/containers/PlatformPusherNotifications/helpers.ts new file mode 100644 index 0000000..7b613db --- /dev/null +++ b/src/containers/PlatformPusherNotifications/helpers.ts @@ -0,0 +1,18 @@ +//typeDefs +import { NewNotificationDetails, Notification } from './interfaces'; + +//utils +import uniqueId from 'lodash/uniqueId'; +import pick from 'lodash/pick'; +import { PausableTimer, OnTimeout } from 'utils/pausableTimer'; + +export function createNewNotificationFromNotifDetails( + notifDetails: NewNotificationDetails, + { onTimeout }: { onTimeout: OnTimeout } +): Notification { + return { + ...pick(notifDetails, ['message', 'type']), + id: uniqueId('notification_object_'), + timer: new PausableTimer(onTimeout, notifDetails.timeout || 5000), + }; +} diff --git a/src/containers/PlatformPusherNotifications/index.tsx b/src/containers/PlatformPusherNotifications/index.tsx new file mode 100644 index 0000000..34b3e51 --- /dev/null +++ b/src/containers/PlatformPusherNotifications/index.tsx @@ -0,0 +1,73 @@ +import React, { memo, useCallback, useMemo, useState } from 'react'; + +//typedefs +import { Notification, PlatformPusherProps } from './interfaces'; +import { OnTimeout } from 'utils'; + +//constants +import { EMPTY_ARRAY_READONLY } from 'config/constants'; + +//utils +import concat from 'lodash/concat'; +import remove from 'lodash/remove'; +import slice from 'lodash/slice'; +import { createNewNotificationFromNotifDetails } from './helpers'; + +//context +import PlatformPusherNotification, { + PlatformPusherNotificationContext, + PushNotification, +} from 'context/PlatformPusherNotification'; + +//components +import NotificationsView from './components/Notifications'; + +const PlatformPusherNotifications: React.FC = props => { + const [notifications, setNotifications] = useState>( + EMPTY_ARRAY_READONLY + ); + + const handleNotifTimeout: OnTimeout = useCallback(timer => { + setNotifications(currNotifications => { + const shallowCopiedNotifications = [...currNotifications]; + remove( + shallowCopiedNotifications, + (notif: Notification): boolean => notif.timer === timer + ); + return shallowCopiedNotifications; + }); + }, []); + + const pushNotification = useCallback( + notifDetails => { + setNotifications(currNotif => + concat( + currNotif, + createNewNotificationFromNotifDetails(notifDetails, { onTimeout: handleNotifTimeout }) + ) + ); + }, + [handleNotifTimeout] + ); + + const context = useMemo( + () => ({ + pushNotification: pushNotification, + }), + [pushNotification] + ); + + const oldestFiveNotifs = useMemo( + () => slice(notifications, 0, 5), + [notifications] + ); + + return ( + + + {props.children} + + ); +}; + +export default memo(PlatformPusherNotifications); diff --git a/src/containers/PlatformPusherNotifications/interfaces.ts b/src/containers/PlatformPusherNotifications/interfaces.ts new file mode 100644 index 0000000..bcdb1bf --- /dev/null +++ b/src/containers/PlatformPusherNotifications/interfaces.ts @@ -0,0 +1,14 @@ +import React from 'react'; +import { PausableTimer } from 'utils'; +import { NOTIFICATION_TYPES } from 'config/interfaces'; + +export * from 'config/interfaces/notifications'; + +export type Notification = { + readonly id: string; + readonly type: NOTIFICATION_TYPES; + readonly message: string; + readonly timer: PausableTimer; +}; + +export type PlatformPusherProps = { children: React.ReactNode }; diff --git a/src/containers/UsersList/components/UsersListView/index.tsx b/src/containers/UsersList/components/UsersListView/index.tsx index 326d7ff..1796dc9 100644 --- a/src/containers/UsersList/components/UsersListView/index.tsx +++ b/src/containers/UsersList/components/UsersListView/index.tsx @@ -30,7 +30,7 @@ const UsersListView = forwardRef((props, ref className={isLoadingForFirstTime ? 'fullPageLoad' : ''} > {isLoadingForFirstTime ? ( - + ) : ( <> {map(users, user => ( diff --git a/src/containers/UsersList/helpers.ts b/src/containers/UsersList/helpers.ts new file mode 100644 index 0000000..3ae3d7a --- /dev/null +++ b/src/containers/UsersList/helpers.ts @@ -0,0 +1,15 @@ +export function getMessageForNotificationPostDataFetch({ + hasError, + total, + currentDataLength, +}: { + readonly hasError: boolean; + readonly total: number; + readonly currentDataLength: number; +}): string { + if (hasError) { + return 'Couldn\'t fetch users'; + } else { + return total === currentDataLength ? 'Fetched all users!' : 'Users fetched, more to go!'; + } +} diff --git a/src/containers/UsersList/index.tsx b/src/containers/UsersList/index.tsx index 27c5364..13bc29c 100644 --- a/src/containers/UsersList/index.tsx +++ b/src/containers/UsersList/index.tsx @@ -1,31 +1,40 @@ -import React, { memo, useCallback, useEffect, useReducer } from 'react'; - +import React, { memo, useCallback, useContext, useEffect, useReducer } from 'react'; //hooks import useInfiniteScroll from 'hooks/useInfiniteScroll'; - +import useHasChanged from 'hooks/useHasChanged'; //typeDefs import { UsersReducer } from './interfaces'; - +import { NOTIFICATION_TYPES } from 'config/interfaces'; //reducer import reducer from './reducer'; - //constants import { INITIAL_USER_STORE } from './constants'; - //thunks import { fetchUsers as fetchUsersThunk } from './thunks'; - //components import UsersListView from './components/UsersListView'; +//context +import PusherNotificationContext, { + PlatformPusherNotificationContext, +} from 'context/PlatformPusherNotification'; +import { getMessageForNotificationPostDataFetch } from './helpers'; const UsersListContainer: React.FC<{}> = () => { const [state, dispatch] = useReducer(reducer, INITIAL_USER_STORE), - { page: currentPage, data, total, isLoading, hasError } = state; + { page: currentPage, data, total, isLoading, hasError } = state, + hasIsLoadingChanged = useHasChanged(isLoading); + + const { pushNotification } = useContext( + PusherNotificationContext + ); const fetchUsers = useCallback<() => void>(() => { const hasMore = total ? data.length < total : true; - !isLoading && hasMore && fetchUsersThunk(dispatch, { page: currentPage + 1 }); - }, [total, data.length, isLoading, currentPage]); + if (!isLoading && hasMore) { + pushNotification({ type: NOTIFICATION_TYPES.INFO, message: 'Fetching Users' }); + fetchUsersThunk(dispatch, { page: currentPage + 1 }); + } + }, [total, data.length, isLoading, currentPage, pushNotification]); const ref = useInfiniteScroll({ isLoading, @@ -38,6 +47,19 @@ const UsersListContainer: React.FC<{}> = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (hasIsLoadingChanged && !isLoading) { + pushNotification({ + type: hasError ? NOTIFICATION_TYPES.ERROR : NOTIFICATION_TYPES.SUCCESS, + message: getMessageForNotificationPostDataFetch({ + hasError, + total: total as number, + currentDataLength: data.length, + }), + }); + } + }, [data.length, hasError, hasIsLoadingChanged, isLoading, pushNotification, total]); + return ; }; diff --git a/src/containers/UsersList/thunks.ts b/src/containers/UsersList/thunks.ts index 478afdf..b589996 100644 --- a/src/containers/UsersList/thunks.ts +++ b/src/containers/UsersList/thunks.ts @@ -2,10 +2,8 @@ import { Dispatch } from 'react'; import { ACTION_TYPES, User, UsersStore } from './interfaces'; import { Action } from 'config/interfaces'; - //constants import { FETCH_USERS_API_PATH } from './constants'; - //utils import { createAction, get } from 'utils'; import map from 'lodash/map'; diff --git a/src/context/PlatformPusherNotification.ts b/src/context/PlatformPusherNotification.ts new file mode 100644 index 0000000..89b0660 --- /dev/null +++ b/src/context/PlatformPusherNotification.ts @@ -0,0 +1,13 @@ +import React from 'react'; + +import { NewNotificationDetails } from 'config/interfaces'; + +export type PushNotification = (notifDetails: NewNotificationDetails) => void; + +export type PlatformPusherNotificationContext = { + pushNotification: PushNotification; +}; + +export default React.createContext( + {} as PlatformPusherNotificationContext +); diff --git a/src/styles/mixins/index.ts b/src/styles/mixins/index.ts index 8dfc920..051ba65 100644 --- a/src/styles/mixins/index.ts +++ b/src/styles/mixins/index.ts @@ -14,6 +14,14 @@ export function circular(): string { return `border-radius: 50%;`; } +export function textEllipsis(): string { + return ` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + `; +} + export function flexContainer({ flexDirection = 'row', alignItems = 'flex-start', diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 88553b2..a5e8dec 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -4,6 +4,10 @@ export const COLORS = { WHITE: 'white' as const, ALTO: '#f2f2f2' as const, DARK_OUTER_SPACE: '#606369' as const, + PASTEL_GREEN: '#35C759' as const, + BITTERSWEET: '#FF6969' as const, + PEACH: '#FFB5B4' as const, + HONEY: '#EB9605' as const, }; export function spacing(ratio: number) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 5fcfdcf..e8e683f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,6 +3,7 @@ import { Action } from 'config/interfaces'; export * from './apiUtils'; export * from './DOMUtils'; +export * from './pausableTimer'; export function createAction(actionType: T, payload: S): Action { return { diff --git a/src/utils/pausableTimer.ts b/src/utils/pausableTimer.ts new file mode 100644 index 0000000..74ab8b8 --- /dev/null +++ b/src/utils/pausableTimer.ts @@ -0,0 +1,43 @@ +export type OnTimeout = (timer: PausableTimer) => void; + +class PausableTimer { + callback: OnTimeout; + remainingTime: number; + lastResumeTime: number | null; + timer: ReturnType | null; + + constructor(onTimeout: OnTimeout, timeout: number) { + this.callback = onTimeout; + this.remainingTime = timeout; + this.lastResumeTime = null; + this.timer = null; + + this.start(); + } + + onTimeout = (): void => { + this.remainingTime = 0; + this.lastResumeTime = null; + this.timer = null; + + this.callback(this); + }; + + start = (): void => { + if (this.remainingTime) { + this.timer = setTimeout(this.onTimeout, this.remainingTime); + this.lastResumeTime = Date.now(); + } + }; + + pause = (): void => { + if (this.timer && this.lastResumeTime) { + clearTimeout(this.timer); + this.remainingTime = this.remainingTime - (Date.now() - this.lastResumeTime); + this.timer = null; + this.lastResumeTime = null; + } + }; +} + +export { PausableTimer }; diff --git a/yarn.lock b/yarn.lock index 51e6ab7..08d2338 100644 --- a/yarn.lock +++ b/yarn.lock @@ -905,7 +905,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.5.1", "@babel/runtime@^7.7.4": +"@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.4": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== @@ -1484,6 +1484,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@^4.2.4": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.2.4.tgz#c7416225987ccdb719262766c1483da8f826838d" + integrity sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16.9.0": version "16.9.26" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.26.tgz#1e55803e468f5393413e29033538cc9aaed6cec9" @@ -3474,7 +3481,7 @@ cssstyle@^1.0.0, cssstyle@^1.1.1: dependencies: cssom "0.3.x" -csstype@^2.2.0: +csstype@^2.2.0, csstype@^2.6.7: version "2.6.9" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== @@ -3728,6 +3735,14 @@ dom-converter@^0.2: dependencies: utila "~0.4" +dom-helpers@^5.0.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" + integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw== + dependencies: + "@babel/runtime" "^7.6.3" + csstype "^2.6.7" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -8716,6 +8731,16 @@ react-scripts@3.4.1: optionalDependencies: fsevents "2.1.2" +react-transition-group@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683" + integrity sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" @@ -8997,6 +9022,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"