diff --git a/apps/example-app/app/app.tsx b/apps/example-app/app/app.tsx index 44901200d..07b81bf68 100644 --- a/apps/example-app/app/app.tsx +++ b/apps/example-app/app/app.tsx @@ -22,7 +22,7 @@ import { useFonts } from "expo-font" import React from "react" import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context" import * as Linking from "expo-linking" -import { useInitialRootStore } from "app/mobxStateTree" +import { useInitialRootStore } from "app/stores/mobxStateTree" import { AppNavigator, useNavigationPersistence } from "app/navigators" import { ErrorBoundary } from "app/screens/ErrorScreen/ErrorBoundary" import * as storage from "app/utils/storage" @@ -30,8 +30,10 @@ import { customFontsToLoad } from "app/theme" import Config from "app/config" import { GestureHandlerRootView } from "react-native-gesture-handler" import { StatusBar, ViewStyle } from "react-native" -import { store } from "app/redux" +import { store } from "app/stores/redux" import { Provider as ReduxProvider } from "react-redux" +import { ApolloProvider } from "@apollo/client" +import { client as apolloClient } from "app/stores/apollo" export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE" @@ -101,17 +103,19 @@ function App(props: AppProps) { // otherwise, we're ready to render the app return ( - - - - - - - + + + + + + + + + ) } diff --git a/apps/example-app/app/devtools/ReactotronConfig.ts b/apps/example-app/app/devtools/ReactotronConfig.ts index a5c7d5b6a..56433b31c 100644 --- a/apps/example-app/app/devtools/ReactotronConfig.ts +++ b/apps/example-app/app/devtools/ReactotronConfig.ts @@ -10,11 +10,13 @@ import { ArgType } from "reactotron-core-client" import { mst } from "reactotron-mst" import apisaucePlugin from "reactotron-apisauce" import { reactotronRedux } from "reactotron-redux" +import apolloPlugin from "reactotron-apollo-client" import { clear } from "app/utils/storage" import { goBack, resetRoot, navigate } from "app/navigators/navigationUtilities" import { Reactotron } from "./ReactotronClient" +import { client } from "../stores/apollo" // <--- update this location const reactotron = Reactotron.configure({ name: require("../../package.json").name, @@ -31,6 +33,7 @@ const reactotron = Reactotron.configure({ filter: (event) => /postProcessSnapshot|@APPLY_SNAPSHOT/.test(event.name) === false, }) ) + .use(apolloPlugin({ apolloClient: client })) if (Platform.OS !== "web") { reactotron.setAsyncStorageHandler?.(AsyncStorage) diff --git a/apps/example-app/app/navigators/AppNavigator.tsx b/apps/example-app/app/navigators/AppNavigator.tsx index b72d59ced..aeda8655e 100644 --- a/apps/example-app/app/navigators/AppNavigator.tsx +++ b/apps/example-app/app/navigators/AppNavigator.tsx @@ -37,6 +37,15 @@ export type AppStackParamList = { MobxStateTree: undefined AsyncStorage: undefined Redux: undefined + Apollo: undefined + ApolloDetail: { + item: { + __typename: string + id: number + number: number | null + title: string + } + } } /** @@ -103,6 +112,8 @@ const AppStack = function AppStack() { options={{ title: "Async Storage" }} /> + + ) diff --git a/apps/example-app/app/screens/ApolloDetailScreen.tsx b/apps/example-app/app/screens/ApolloDetailScreen.tsx new file mode 100644 index 000000000..6bca4ad08 --- /dev/null +++ b/apps/example-app/app/screens/ApolloDetailScreen.tsx @@ -0,0 +1,99 @@ +import React from "react" +import { FlatList, TextStyle, View, ViewStyle } from "react-native" +import { ListItem, Text } from "app/components" +import { AppStackScreenProps } from "app/navigators" +import { colors, spacing } from "app/theme" +import { useSafeAreaInsetsStyle } from "app/utils/useSafeAreaInsetsStyle" +import { gql, useQuery } from "@apollo/client" +import { observer } from "mobx-react-lite" + +const SECTIONS_QUERY = gql` + query Sections($id: Int!) { + chapter(id: $id) { + sections { + number + title + } + } + } +` + +interface Section { + number: number + title: string +} + +interface SectionItemProps { + chapter: { + __typename: string + id: number + number: number | null + title: string + } + section: Section + onPress?: () => void +} + +const SectionItem: React.FC = ({ chapter, section, onPress }) => ( + +) + +interface ApolloDetailScreenProps extends AppStackScreenProps<"ApolloDetail"> {} + +export const ApolloDetailScreen: React.FC = observer( + function ApolloScreen({ route }) { + const id = route.params.item.id + + const { data, loading } = useQuery(SECTIONS_QUERY, { + variables: { id }, + }) + + const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"]) + + return ( + } + ListHeaderComponent={() => { + return ( + + + + + + ) + }} + keyExtractor={(section) => section.number.toString()} + /> + ) + } +) + +const $container: ViewStyle = { + flex: 1, + backgroundColor: colors.background, +} + +const $text: TextStyle = { + color: colors.text, +} +const $subheading: TextStyle = { + ...$text, + margin: spacing.sm, +} + +const $bottomBorder: ViewStyle = { + borderBottomWidth: 1, + borderBottomColor: colors.text, + marginHorizontal: spacing.sm, +} diff --git a/apps/example-app/app/screens/ApolloScreen.tsx b/apps/example-app/app/screens/ApolloScreen.tsx new file mode 100644 index 000000000..d31bc77c8 --- /dev/null +++ b/apps/example-app/app/screens/ApolloScreen.tsx @@ -0,0 +1,99 @@ +import React from "react" +import { FlatList, TextStyle, View, ViewStyle } from "react-native" +import { ListItem, Text } from "app/components" +import { AppStackScreenProps } from "app/navigators" +import { colors, spacing } from "app/theme" +import { useSafeAreaInsetsStyle } from "app/utils/useSafeAreaInsetsStyle" +import { gql, useQuery } from "@apollo/client" +import { useNavigation } from "@react-navigation/native" +import { observer } from "mobx-react-lite" + +const CHAPTERS_QUERY = gql` + query Chapters { + chapters { + id + number + title + } + } +` + +const ChapterItem = ({ + chapter, + onPress, +}: { + chapter: { id: number; number: number; title: string } + onPress?: () => void +}) => { + const { number, title } = chapter + let header, subheader + + if (number) { + header = `Chapter ${number}` + subheader = ` - ${title}` + } else { + header = title + subheader = "" + } + + return ( + + ) +} + +interface ApolloScreenProps extends AppStackScreenProps<"Apollo"> {} + +export const ApolloScreen: React.FC = observer(function ApolloScreen() { + const { data, loading } = useQuery(CHAPTERS_QUERY) + const navigation = useNavigation() + + const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"]) + + return ( + ( + navigation.navigate("ApolloDetail", { item })} /> + )} + ListHeaderComponent={() => { + return ( + + + + + + ) + }} + keyExtractor={(chapter) => chapter.id.toString()} + /> + ) +}) + +const $container: ViewStyle = { + flex: 1, + backgroundColor: colors.background, +} + +const $text: TextStyle = { + color: colors.text, +} +const $subheading: TextStyle = { + ...$text, + margin: spacing.sm, +} + +const $bottomBorder: ViewStyle = { + borderBottomWidth: 1, + borderBottomColor: colors.text, + marginHorizontal: spacing.sm, +} diff --git a/apps/example-app/app/screens/ErrorGeneratorScreen.tsx b/apps/example-app/app/screens/ErrorGeneratorScreen.tsx index 95440e0ae..3df6f456c 100644 --- a/apps/example-app/app/screens/ErrorGeneratorScreen.tsx +++ b/apps/example-app/app/screens/ErrorGeneratorScreen.tsx @@ -4,8 +4,8 @@ import { useDispatch } from "react-redux" import { Button, Text } from "app/components" import type { AppStackScreenProps } from "app/navigators" import { colors, spacing } from "app/theme" -import type { AppDispatch } from "app/redux" -import { throwAnError, throwErrorAsync } from "app/redux/errorSlice" +import type { AppDispatch } from "app/stores/redux" +import { throwAnError, throwErrorAsync } from "app/stores/redux/errorSlice" import { useSafeAreaInsetsStyle } from "app/utils/useSafeAreaInsetsStyle" interface ErrorGeneratorScreenProps extends AppStackScreenProps<"ErrorGenerator"> {} diff --git a/apps/example-app/app/screens/MobxStateTreeScreen.tsx b/apps/example-app/app/screens/MobxStateTreeScreen.tsx index 2b44d3fe7..3a6bf0ec9 100644 --- a/apps/example-app/app/screens/MobxStateTreeScreen.tsx +++ b/apps/example-app/app/screens/MobxStateTreeScreen.tsx @@ -4,7 +4,7 @@ import { ScrollView, TextStyle, View, ViewStyle } from "react-native" import { Button, Text } from "app/components" import { AppStackScreenProps } from "app/navigators" import { colors, spacing } from "app/theme" -import { useStores } from "app/mobxStateTree" +import { useStores } from "app/stores/mobxStateTree" import { Repo } from "app/components/Repo" import { useSafeAreaInsetsStyle } from "app/utils/useSafeAreaInsetsStyle" diff --git a/apps/example-app/app/screens/ReduxScreen.tsx b/apps/example-app/app/screens/ReduxScreen.tsx index 628103caf..1dd48b6ed 100644 --- a/apps/example-app/app/screens/ReduxScreen.tsx +++ b/apps/example-app/app/screens/ReduxScreen.tsx @@ -5,9 +5,9 @@ import { Button, Text } from "app/components" import { Repo } from "app/components/Repo" import { AppStackScreenProps } from "app/navigators" import { colors, spacing } from "app/theme" -import type { AppDispatch, RootState } from "app/redux" -import { fetchAsync, reset as repoReset } from "app/redux/repoSlice" -import { changeSize, changeSpeed, reset as logoReset } from "app/redux/logoSlice" +import type { AppDispatch, RootState } from "app/stores/redux" +import { fetchAsync, reset as repoReset } from "app/stores/redux/repoSlice" +import { changeSize, changeSpeed, reset as logoReset } from "app/stores/redux/logoSlice" import { useSafeAreaInsetsStyle } from "app/utils/useSafeAreaInsetsStyle" interface ReduxScreenProps extends AppStackScreenProps<"Redux"> {} diff --git a/apps/example-app/app/screens/WelcomeScreen.tsx b/apps/example-app/app/screens/WelcomeScreen.tsx index d6219da14..e9ad76c3c 100644 --- a/apps/example-app/app/screens/WelcomeScreen.tsx +++ b/apps/example-app/app/screens/WelcomeScreen.tsx @@ -72,6 +72,12 @@ export const WelcomeScreen: React.FC = function WelcomeScree navigation.navigate("Redux") }} /> + { + navigation.navigate("Apollo") + }} + /> diff --git a/apps/example-app/app/screens/index.ts b/apps/example-app/app/screens/index.ts index f9bdc3c61..72afb2a29 100644 --- a/apps/example-app/app/screens/index.ts +++ b/apps/example-app/app/screens/index.ts @@ -7,6 +7,8 @@ export * from "./MobxStateTreeScreen" export * from "./ReduxScreen" export * from "./ErrorGeneratorScreen" export * from "./AsyncStorageScreen" +export * from "./ApolloScreen" +export * from "./ApolloDetailScreen" export * from "./ErrorScreen/ErrorBoundary" // export other screens here diff --git a/apps/example-app/app/stores/apollo/index.tsx b/apps/example-app/app/stores/apollo/index.tsx new file mode 100644 index 000000000..bb7375fe4 --- /dev/null +++ b/apps/example-app/app/stores/apollo/index.tsx @@ -0,0 +1,10 @@ +import { ApolloClient, InMemoryCache } from "@apollo/client" + +const cache = new InMemoryCache() + +export const client = new ApolloClient({ + uri: "https://api.graphql.guide/graphql", + cache, + defaultOptions: { watchQuery: { fetchPolicy: "cache-and-network" } }, + connectToDevTools: true, +}) diff --git a/apps/example-app/app/mobxStateTree/LogoStore.ts b/apps/example-app/app/stores/mobxStateTree/LogoStore.ts similarity index 100% rename from apps/example-app/app/mobxStateTree/LogoStore.ts rename to apps/example-app/app/stores/mobxStateTree/LogoStore.ts diff --git a/apps/example-app/app/mobxStateTree/RepoStore.ts b/apps/example-app/app/stores/mobxStateTree/RepoStore.ts similarity index 100% rename from apps/example-app/app/mobxStateTree/RepoStore.ts rename to apps/example-app/app/stores/mobxStateTree/RepoStore.ts diff --git a/apps/example-app/app/mobxStateTree/RootStore.ts b/apps/example-app/app/stores/mobxStateTree/RootStore.ts similarity index 100% rename from apps/example-app/app/mobxStateTree/RootStore.ts rename to apps/example-app/app/stores/mobxStateTree/RootStore.ts diff --git a/apps/example-app/app/mobxStateTree/helpers/getRootStore.ts b/apps/example-app/app/stores/mobxStateTree/helpers/getRootStore.ts similarity index 100% rename from apps/example-app/app/mobxStateTree/helpers/getRootStore.ts rename to apps/example-app/app/stores/mobxStateTree/helpers/getRootStore.ts diff --git a/apps/example-app/app/mobxStateTree/helpers/setupRootStore.ts b/apps/example-app/app/stores/mobxStateTree/helpers/setupRootStore.ts similarity index 97% rename from apps/example-app/app/mobxStateTree/helpers/setupRootStore.ts rename to apps/example-app/app/stores/mobxStateTree/helpers/setupRootStore.ts index aea5d36a0..dd3bded63 100644 --- a/apps/example-app/app/mobxStateTree/helpers/setupRootStore.ts +++ b/apps/example-app/app/stores/mobxStateTree/helpers/setupRootStore.ts @@ -11,7 +11,7 @@ */ import { applySnapshot, IDisposer, onSnapshot } from "mobx-state-tree" import { RootStore, RootStoreSnapshot } from "../RootStore" -import * as storage from "../../utils/storage" +import * as storage from "app/utils/storage" /** * The key we'll be saving our state as within async storage. diff --git a/apps/example-app/app/mobxStateTree/helpers/useStores.ts b/apps/example-app/app/stores/mobxStateTree/helpers/useStores.ts similarity index 100% rename from apps/example-app/app/mobxStateTree/helpers/useStores.ts rename to apps/example-app/app/stores/mobxStateTree/helpers/useStores.ts diff --git a/apps/example-app/app/mobxStateTree/index.ts b/apps/example-app/app/stores/mobxStateTree/index.ts similarity index 100% rename from apps/example-app/app/mobxStateTree/index.ts rename to apps/example-app/app/stores/mobxStateTree/index.ts diff --git a/apps/example-app/app/redux/errorSlice.ts b/apps/example-app/app/stores/redux/errorSlice.ts similarity index 100% rename from apps/example-app/app/redux/errorSlice.ts rename to apps/example-app/app/stores/redux/errorSlice.ts diff --git a/apps/example-app/app/redux/index.ts b/apps/example-app/app/stores/redux/index.ts similarity index 74% rename from apps/example-app/app/redux/index.ts rename to apps/example-app/app/stores/redux/index.ts index 4a83c83d2..5ab9e7419 100644 --- a/apps/example-app/app/redux/index.ts +++ b/apps/example-app/app/stores/redux/index.ts @@ -1,12 +1,12 @@ import { configureStore } from "@reduxjs/toolkit" import type { GetDefaultEnhancers } from "@reduxjs/toolkit/dist/getDefaultEnhancers" -import logoReducer from "../redux/logoSlice" -import repoReducer from "../redux/repoSlice" -import errorReducer from "../redux/errorSlice" +import logoReducer from "./logoSlice" +import repoReducer from "./repoSlice" +import errorReducer from "./errorSlice" const createEnhancers = (getDefaultEnhancers: GetDefaultEnhancers) => { if (__DEV__) { - const reactotron = require("../devtools/ReactotronConfig").default + const reactotron = require("../../devtools/ReactotronConfig").default return getDefaultEnhancers().concat(reactotron.createEnhancer()) } else { return getDefaultEnhancers() diff --git a/apps/example-app/app/redux/logoSlice.ts b/apps/example-app/app/stores/redux/logoSlice.ts similarity index 100% rename from apps/example-app/app/redux/logoSlice.ts rename to apps/example-app/app/stores/redux/logoSlice.ts diff --git a/apps/example-app/app/redux/repoSlice.ts b/apps/example-app/app/stores/redux/repoSlice.ts similarity index 100% rename from apps/example-app/app/redux/repoSlice.ts rename to apps/example-app/app/stores/redux/repoSlice.ts diff --git a/apps/example-app/package.json b/apps/example-app/package.json index ad33bf508..bb279325a 100644 --- a/apps/example-app/package.json +++ b/apps/example-app/package.json @@ -33,6 +33,7 @@ "prebuild": "npx expo prebuild" }, "dependencies": { + "@apollo/client": "^3.9.4", "@expo-google-fonts/space-grotesk": "^0.2.2", "@react-native-async-storage/async-storage": "1.23.1", "@react-navigation/native": "^6.0.2", @@ -49,6 +50,7 @@ "expo-localization": "~15.0.3", "expo-splash-screen": "~0.27.7", "expo-status-bar": "~1.12.1", + "graphql": "^16.8.1", "i18n-js": "3.9.2", "mobx": "6.10.2", "mobx-react-lite": "4.0.5", @@ -98,6 +100,7 @@ "postinstall-prepare": "1.0.1", "prettier": "2.8.8", "react-test-renderer": "18.2.0", + "reactotron-apollo-client": "workspace:*", "reactotron-core-client": "workspace:*", "reactotron-mst": "workspace:*", "reactotron-react-js": "workspace:*", diff --git a/apps/reactotron-app/package.json b/apps/reactotron-app/package.json index 60753ae1c..989de29a8 100644 --- a/apps/reactotron-app/package.json +++ b/apps/reactotron-app/package.json @@ -65,6 +65,7 @@ "react-motion": "0.5.2", "react-router-dom": "^6.18.0", "react-tooltip": "4.5.1", + "react-transition-group": "^4.4.5", "reactotron-core-contract": "workspace:*", "reactotron-core-server": "workspace:*", "reactotron-core-ui": "workspace:*", diff --git a/apps/reactotron-app/src/renderer/App.tsx b/apps/reactotron-app/src/renderer/App.tsx index bbab99b60..20b99703e 100644 --- a/apps/reactotron-app/src/renderer/App.tsx +++ b/apps/reactotron-app/src/renderer/App.tsx @@ -15,6 +15,7 @@ import Overlay from "./pages/reactNative/Overlay" import Storybook from "./pages/reactNative/Storybook" import CustomCommands from "./pages/customCommands" import Help from "./pages/help" +import Cache from "./pages/apolloClient/Cache" const AppContainer = styled.div` position: absolute; @@ -67,6 +68,12 @@ function App() { {/* Custom Commands */} } /> + {/* TODO: Load custom UI pages from installed plugins */} + } /> + + {/* } /> + } /> */} + {/* Help */} } /> diff --git a/apps/reactotron-app/src/renderer/ReactotronBrain.tsx b/apps/reactotron-app/src/renderer/ReactotronBrain.tsx index 170a73741..6ff2be370 100644 --- a/apps/reactotron-app/src/renderer/ReactotronBrain.tsx +++ b/apps/reactotron-app/src/renderer/ReactotronBrain.tsx @@ -7,6 +7,7 @@ import { ReactNativeProvider, TimelineProvider, StateProvider, + ApolloClientProvider, } from "reactotron-core-ui" import KeybindHandler from "./KeybindHandler" @@ -37,7 +38,9 @@ const ReactotronBrain: FunctionComponent> = ({ + {children} + diff --git a/apps/reactotron-app/src/renderer/components/ConnectionSelector/ConnectionSelector.story.tsx b/apps/reactotron-app/src/renderer/components/ConnectionSelector/ConnectionSelector.story.tsx index 2b7f47204..90292ff47 100644 --- a/apps/reactotron-app/src/renderer/components/ConnectionSelector/ConnectionSelector.story.tsx +++ b/apps/reactotron-app/src/renderer/components/ConnectionSelector/ConnectionSelector.story.tsx @@ -16,6 +16,7 @@ storiesOf("components/ConnectionSelector", module) platform: "ios", commands: [], connected: true, + plugins: [], }} onClick={() => {}} /> @@ -31,6 +32,7 @@ storiesOf("components/ConnectionSelector", module) platform: "android", commands: [], connected: true, + plugins: [], }} onClick={() => {}} /> @@ -45,6 +47,7 @@ storiesOf("components/ConnectionSelector", module) platform: "ios", commands: [], connected: true, + plugins: [], }} connection={{ id: 0, @@ -54,6 +57,7 @@ storiesOf("components/ConnectionSelector", module) platform: "ios", commands: [], connected: true, + plugins: [], }} onClick={() => {}} /> @@ -69,6 +73,7 @@ storiesOf("components/ConnectionSelector", module) platform: "ios", commands: [], connected: true, + plugins: [], }} connection={{ id: 0, @@ -79,6 +84,7 @@ storiesOf("components/ConnectionSelector", module) platform: "ios", commands: [], connected: true, + plugins: [], }} onClick={() => {}} /> diff --git a/apps/reactotron-app/src/renderer/components/Footer/Footer.story.tsx b/apps/reactotron-app/src/renderer/components/Footer/Footer.story.tsx index 4119bb035..c66195b0f 100644 --- a/apps/reactotron-app/src/renderer/components/Footer/Footer.story.tsx +++ b/apps/reactotron-app/src/renderer/components/Footer/Footer.story.tsx @@ -12,6 +12,7 @@ const testConnections: Connection[] = [ clientId: "1", name: "Test 1", commands: [], + plugins: [], platform: "ios", platformVersion: "12", connected: true, @@ -21,6 +22,7 @@ const testConnections: Connection[] = [ clientId: "1", name: "Test 2", commands: [], + plugins: [], platform: "ios", platformVersion: "12", connected: true, @@ -30,6 +32,7 @@ const testConnections: Connection[] = [ clientId: "1", name: "Test 3", commands: [], + plugins: [], platform: "ios", platformVersion: "12", connected: true, @@ -39,6 +42,7 @@ const testConnections: Connection[] = [ clientId: "1", name: "Test 4", commands: [], + plugins: [], platform: "ios", platformVersion: "12", connected: true, @@ -48,6 +52,7 @@ const testConnections: Connection[] = [ clientId: "1", name: "Test 5", commands: [], + plugins: [], platform: "ios", platformVersion: "12", connected: true, diff --git a/apps/reactotron-app/src/renderer/components/SideBar/SideBar.story.tsx b/apps/reactotron-app/src/renderer/components/SideBar/SideBar.story.tsx index 49fd1742d..df082ef54 100644 --- a/apps/reactotron-app/src/renderer/components/SideBar/SideBar.story.tsx +++ b/apps/reactotron-app/src/renderer/components/SideBar/SideBar.story.tsx @@ -9,6 +9,6 @@ export default { export const Default = () => ( - + ) diff --git a/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx b/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx index 2ff34b503..fb7b95f04 100644 --- a/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx +++ b/apps/reactotron-app/src/renderer/components/SideBar/Sidebar.tsx @@ -9,11 +9,13 @@ import { MdMobiledataOff, } from "react-icons/md" import { FaMagic } from "react-icons/fa" +import { SiApollographql } from "react-icons/si" import styled from "styled-components" import SideBarButton from "../SideBarButton" import { reactotronLogo } from "../../images" import { ServerStatus } from "../../contexts/Standalone/useStandalone" +import { Transition } from "react-transition-group" interface SideBarContainerProps { $isOpen: boolean @@ -33,7 +35,20 @@ const Spacer = styled.div` flex: 1; ` -function SideBar({ isOpen, serverStatus }: { isOpen: boolean; serverStatus: ServerStatus }) { +const transitionStyles = { + entering: { opacity: 0 }, + entered: { opacity: 1 }, + exiting: { opacity: 1 }, + exited: { opacity: 0 }, +} + +interface SideBarProps { + isOpen: boolean + serverStatus: ServerStatus + plugins: string[] +} + +function SideBar({ isOpen, serverStatus, plugins }: SideBarProps) { let serverIcon = MdMobiledataOff let iconColor let serverText = "Stopped" @@ -54,6 +69,11 @@ function SideBar({ isOpen, serverStatus }: { isOpen: boolean; serverStatus: Serv } } + const hasApolloClient = React.useMemo( + () => plugins.find((plugin) => plugin === "apollo-client"), + [plugins] + ) + return ( @@ -72,6 +92,25 @@ function SideBar({ isOpen, serverStatus }: { isOpen: boolean; serverStatus: Serv /> + + {(state) => ( +
+ +
+ )} +
+ + const standaloneContext = useContext(StandaloneContext) + const { serverStatus, selectedConnection } = standaloneContext + + return ( + + ) } export default SideBar diff --git a/apps/reactotron-app/src/renderer/contexts/Standalone/useStandalone.test.ts b/apps/reactotron-app/src/renderer/contexts/Standalone/useStandalone.test.ts index 195f0febc..6635f1e5c 100644 --- a/apps/reactotron-app/src/renderer/contexts/Standalone/useStandalone.test.ts +++ b/apps/reactotron-app/src/renderer/contexts/Standalone/useStandalone.test.ts @@ -15,6 +15,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -24,6 +25,7 @@ describe("contexts/Standalone/useStandalone", () => { id: 0, platform: "ios", commands: [], + plugins: [], connected: true, }) expect(result.current.selectedClientId).toEqual("1234") @@ -39,6 +41,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -48,6 +51,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -57,6 +61,7 @@ describe("contexts/Standalone/useStandalone", () => { id: 0, platform: "ios", commands: [], + plugins: [], connected: true, }) }) @@ -73,6 +78,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -95,6 +101,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -105,6 +112,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "12345", id: 1, platform: "ios", + plugins: [], }) }) @@ -119,6 +127,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -129,6 +138,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -143,6 +153,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -151,6 +162,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "567", id: 1, platform: "ios", + plugins: [], }) }) @@ -161,6 +173,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "567", id: 1, platform: "ios", + plugins: [], }) }) @@ -175,6 +188,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -183,6 +197,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "567", id: 1, platform: "ios", + plugins: [], }) }) @@ -193,6 +208,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -207,6 +223,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -217,6 +234,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -231,6 +249,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -251,6 +270,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -259,6 +279,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "456", id: 1, platform: "ios", + plugins: [], }) }) @@ -294,6 +315,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) @@ -320,6 +342,7 @@ describe("contexts/Standalone/useStandalone", () => { clientId: "1234", id: 0, platform: "ios", + plugins: [], }) }) diff --git a/apps/reactotron-app/src/renderer/contexts/Standalone/useStandalone.ts b/apps/reactotron-app/src/renderer/contexts/Standalone/useStandalone.ts index 62ee1f3d2..2a57dd180 100644 --- a/apps/reactotron-app/src/renderer/contexts/Standalone/useStandalone.ts +++ b/apps/reactotron-app/src/renderer/contexts/Standalone/useStandalone.ts @@ -26,6 +26,7 @@ export interface ReactotronConnection { platformVersion?: string osRelease?: string userAgent?: string + plugins: string[] } export interface Connection extends ReactotronConnection { diff --git a/apps/reactotron-app/src/renderer/pages/apolloClient/Cache.tsx b/apps/reactotron-app/src/renderer/pages/apolloClient/Cache.tsx new file mode 100644 index 000000000..000073a14 --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/apolloClient/Cache.tsx @@ -0,0 +1,545 @@ +import React, { useCallback, useContext, useEffect } from "react" +import styled, { useTheme } from "styled-components" +import { + Header, + ReactotronContext, + TreeView, + ApolloClientContext, + Checkbox, + Tooltip, + ApolloUpdateCacheValueModal, +} from "reactotron-core-ui" +import { TbDatabaseDollar } from "react-icons/tb" +import { Title } from "../reactNative/components/Shared" +import { CommandType } from "reactotron-core-contract" +import type { ApolloClientCacheUpdatePayload } from "reactotron-core-contract" +import { + FaArrowLeft, + FaArrowRight, + FaCopy, + FaEdit, + FaExternalLinkAlt, + FaTimes, +} from "react-icons/fa" +import { PiPushPinFill, PiPushPinSlash } from "react-icons/pi" +import { Link, useNavigate, useParams } from "react-router-dom" +import { clipboard, shell } from "electron" + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; +` + +const CacheContainer = styled.div` + display: flex; + min-height: 100vh; + width: 100%; +` + +const TopSection = styled.div` + display: flex; + flex-direction: row; + padding-left: 10px; +` + +const SearchContainer = styled.div` + display: flex; + align-items: center; + padding-bottom: 10px; + padding-top: 4px; + padding-left: 10px; +` + +const SearchInput = styled.input` + border-radius: 4px; + padding: 10px; + flex: 1; + background-color: ${(props) => props.theme.backgroundSubtleDark}; + border: none; + color: ${(props) => props.theme.foregroundDark}; + font-size: 14px; +` +export const ButtonContainer = styled.div` + padding: 10px; + cursor: pointer; +` + +const ButtonContainerDisabled = styled.div` + padding: 10px; + cursor: not-allowed; +` + +const Row = styled.div` + display: flex; + flex-direction: row; +` + +const IconContainer = styled.span` + padding-left: 10px; +` + +const LeftPanel = styled.div` + overflow-y: auto; + max-height: 100%; + flex-direction: column; + flex: 1; + padding: 10px; + justify-content: flex-start; + padding-bottom: 250px; +` + +const RightPanel = styled.div` + flex-direction: column; + flex: 2; + padding: 10px; + overflow-y: auto; + max-height: 100%; + padding-bottom: 250px; + background-color: ${(props) => props.theme.backgroundSubtleDark}; +` + +const SpanContainer = styled.span`` + +const StyledLink = styled(Link)` + color: ${(props) => props.theme.constant}; +` + +const CacheKeyLink = styled(Link)` + padding: 10px; + text-decoration: none; +` + +const SelectedCacheKeyLink = styled(CacheKeyLink)` + background-color: ${(props) => props.theme.backgroundSubtleDark}; +` + +const CacheKeyLabel = styled.span` + font-size: 14px; + color: ${(props) => props.theme.foregroundDark}; +` + +const Highlight = styled.span` + background-color: yellow; + color: black; +` + +const VerticalContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; +` + +const PinnedSeparator = styled.div` + border-top: 1px solid ${(props) => props.theme.chromeLine}; + margin: 10px 0; +` + +const READ_ONLY_FIELDS = ["__typename", "id"] + +const HighlightText = ({ text, searchTerm }) => { + try { + const parts = text.toString().split(new RegExp(`(${searchTerm})`, "gi")) + return ( + <> + {parts.map((part, index) => + part.toLowerCase() === searchTerm.toLowerCase() ? ( + {part} + ) : ( + part + ) + )} + + ) + } catch (error) { + return text + } +} + +function Cache() { + const theme = useTheme() + + const { sendCommand } = React.useContext(ReactotronContext) + const { + closeSearch, + setSearch, + search, + viewedKeys, + setViewedKeys, + currentIndex, + goForward, + goBack, + getCurrentKey, + pinnedKeys, + togglePin, + data, + isEditOpen, + closeEdit, + openEdit, + } = useContext(ApolloClientContext) + + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + setSearch(e.target.value) + }, + [setSearch] + ) + + const { cacheKey: routeKey } = useParams() + const navigate = useNavigate() + /** + * if we have unmounted via another tab press, + * restore the last key we were viewing when user returns here + */ + useEffect(() => { + if (!routeKey) { + const lastItem = getCurrentKey() + if (lastItem) { + navigate(`/apolloClient/cache/${lastItem}`) + } + } + }, [routeKey, getCurrentKey, navigate]) + + useEffect(() => { + // Check if cacheKey is new + const currentItem = getCurrentKey() + if (routeKey && currentItem !== routeKey) { + // TODO rename `setViewedKeys` to `addKeyToHistory` + setViewedKeys(routeKey) + } + }, [routeKey, setViewedKeys, getCurrentKey]) + + function openURL(url) { + shell.openExternal(url) + } + + const cacheKey = getCurrentKey() ?? routeKey + const cacheData = data.cache[cacheKey] ?? undefined + + const forwardDisabled = currentIndex === viewedKeys.length - 1 + const backDisabled = currentIndex <= 0 + const ForwardButtonWrapper = forwardDisabled ? ButtonContainerDisabled : ButtonContainer + const BackButtonWrapper = backDisabled ? ButtonContainerDisabled : ButtonContainer + + const clearSearch = () => { + if (search === "") { + closeSearch() + } else { + setSearch("") + } + } + + const handleEditKeyValue = useCallback( + (fieldName: string) => { + // identify the possible composite key fields + const keyFields = identifyKeyFields(cacheKey, cacheData) + const identifier = {} + keyFields.forEach((keyField) => { + identifier[keyField] = cacheData[keyField] + }) + + if (keyFields.length > 0) { + const updates: ApolloClientCacheUpdatePayload = { + // @ts-expect-error fix this + typename: cacheData.__typename, + identifier, + fieldName, + fieldValue: cacheData[fieldName], + } + + setInitialValue(updates) + openEdit() + } else { + // we need to prompt the user to something to help identify the key for this object + // otherwise we can't properly update the cache + } + }, + [cacheData, cacheKey, openEdit] + ) + + // TODO add these options to the context in order to not lose state on tab switch + // TODO also add an option for the poll time? + const [searchObjects, setSearchObjects] = React.useState(false) + const [expandInitially, setExpandInitially] = React.useState(true) + const [initialValue, setInitialValue] = React.useState({ + fieldValue: "", + typename: "", + identifier: {}, + fieldName: "", + }) + + const valueRenderer = (transformed: any, untransformed: any, ...keyPath: any) => { + if (keyPath[0] === "__ref") { + return ( + + {untransformed || transformed} + + + + + ) + } else { + let onClick + if (typeof untransformed === "string" && untransformed.startsWith("http")) { + onClick = () => openURL(untransformed) + } + + if (searchObjects && search) { + return ( + + + {!!onClick && ( + + + + )} + + {/* TODO don't show edit button for __typename and any key fields */} + {cacheKey === "ROOT_QUERY" || + (!READ_ONLY_FIELDS.includes(keyPath[0]) && ( + handleEditKeyValue(keyPath[0])} + style={{ display: "inline", padding: "0 5px" }} + > + + + ))} + + ) + } else { + return ( + + {untransformed || transformed} + {!!onClick && ( + + + + )} + + {/* TODO don't show edit button for __typename and any key fields */} + {cacheKey === "ROOT_QUERY" || + (!READ_ONLY_FIELDS.includes(keyPath[0]) && ( + handleEditKeyValue(keyPath[0])} + style={{ display: "inline", padding: "0 5px" }} + > + + + ))} + + ) + } + } + } + + return ( + +
{}, + }, + /* TODO Add queries and mutations tabs up top */ + // { + // text: "Queries", + // icon: HiDocumentSearch, + // isActive: false, + // onClick: () => { + // // TODO: Couldn't get react-router-dom to do it for me so I forced it. + // window.location.hash = "#/apolloClient/queries" + // }, + // }, + // { + // text: "Mutations", + // icon: HiOutlinePencilAlt, + // isActive: false, + // onClick: () => { + // // TODO: Couldn't get react-router-dom to do it for me so I forced it. + // window.location.hash = "#/apolloClient/mutations" + // }, + // }, + ]} + > + + + + + + + + + + setSearchObjects(!searchObjects)} + isChecked={searchObjects} + /> + setExpandInitially(!expandInitially)} + isChecked={expandInitially} + /> + + + +
+ + + {/* always show pinnedKeys */} + {pinnedKeys.map((key) => { + const LinkWrapper = key === cacheKey ? SelectedCacheKeyLink : CacheKeyLink + return ( + + pushViewedKey(key)} + to={`/apolloClient/cache/${key}`} + > + + + + + + togglePin(key)}> + + + + ) + })} + + {pinnedKeys.length > 0 && } + + {Object.keys(data.cache) + .filter((key) => { + if (search) { + if (searchObjects) { + const searchDataJson = JSON.stringify(data.cache[key]) + return searchDataJson.toLowerCase().includes(search.toLowerCase()) + } else { + return key.toLowerCase().includes(search.toLowerCase()) + } + } + + // check key is not pinned + if (pinnedKeys.includes(key)) { + return false + } + + return key + }) + .map((key: string) => { + const LinkWrapper = key === cacheKey ? SelectedCacheKeyLink : CacheKeyLink + return ( + + + + {!searchObjects ? ( + + ) : ( + {key} + )} + + + + togglePin(key)}> + + + + ) + })} + + {cacheData && ( + + + + + + + + + + + {cacheData !== undefined && ( + { + clipboard.writeText(JSON.stringify(cacheData, null, 2)) + }} + > + + + )} + + + Cache ID: {cacheKey} + + + + + )} + + + { + closeEdit() + setInitialValue({ fieldValue: "", typename: "", identifier: {}, fieldName: "" }) + }} + onDispatchAction={(updates) => { + sendCommand(CommandType.ApolloClientUpdateCache, updates) + }} + initialValue={initialValue} + cacheKey={cacheKey} + /> +
+ ) +} + +export default Cache + +function identifyKeyFields(cacheKey, cacheObject) { + if (!cacheKey || !cacheObject) { + return [] // Early exit if no data is provided + } + + // Assuming the cacheKey format could be something like "User:john:01012000" + const keyParts = cacheKey.split(":") + + keyParts.shift() // Remove the typename part + const identifiers = keyParts // Remaining parts are the identifiers + + const keyFields = [] + + // Loop through each identifier and match it against the object's properties + identifiers.forEach((identifier) => { + for (const [key, value] of Object.entries(cacheObject)) { + if (value && value.toString() === identifier && !keyFields.includes(key)) { + keyFields.push(key) // Add matching key field if not already included + } + } + }) + + return keyFields // Return an array of key fields +} diff --git a/apps/reactotron-app/src/renderer/pages/apolloClient/Mutations.tsx b/apps/reactotron-app/src/renderer/pages/apolloClient/Mutations.tsx new file mode 100644 index 000000000..fc758610d --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/apolloClient/Mutations.tsx @@ -0,0 +1,117 @@ +import React from "react" +import styled from "styled-components" +import { Header } from "reactotron-core-ui" +import { MdWarning } from "react-icons/md" +import { HiDocumentSearch, HiOutlinePencilAlt } from "react-icons/hi" +import { TbDatabaseDollar } from "react-icons/tb" +import { Title } from "../reactNative/components/Shared" + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; +` + +const StorybookContainer = styled.div` + display: flex; + flex-direction: column; + height: 100%; +` + +const TopSection = styled.div` + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +` + +const WarningContainer = styled.div` + display: flex; + color: ${(props) => props.theme.warning}; + background-color: ${(props) => props.theme.backgroundDarker}; + border-top: 1px solid ${(props) => props.theme.chromeLine}; + align-items: center; + padding: 0 20px; +` +const WarningDescription = styled.div` + margin-left: 20px; +` + +function Mutations() { + return ( + +
{ + // TODO: Couldn't get react-router-dom to do it for me so I forced it. + window.location.hash = "#/apolloClient/cache" + }, + }, + { + text: "Queries", + icon: HiDocumentSearch, + isActive: false, + onClick: () => { + // TODO: Couldn't get react-router-dom to do it for me so I forced it. + window.location.hash = "#/apolloClient/queries" + }, + }, + { + text: "Mutations", + icon: HiOutlinePencilAlt, + isActive: true, + // eslint-disable-next-line @typescript-eslint/no-empty-function + onClick: () => {}, + }, + ]} + // actions={[ + // { + // tip: "Search", + // icon: MdSearch, + // onClick: () => { + // toggleSearch() + // }, + // }, + // { + // tip: "Filter", + // icon: MdFilterList, + // onClick: () => { + // openFilter() + // }, + // }, + // { + // tip: "Reverse Order", + // icon: MdSwapVert, + // onClick: () => { + // toggleReverse() + // }, + // }, + // { + // tip: "Clear", + // icon: MdDeleteSweep, + // onClick: () => { + // clearSelectedConnectionCommands() + // }, + // }, + // ]} + /> + + + TODO + + + + This is preview feature. + + + + ) +} + +export default Mutations diff --git a/apps/reactotron-app/src/renderer/pages/apolloClient/Queries.tsx b/apps/reactotron-app/src/renderer/pages/apolloClient/Queries.tsx new file mode 100644 index 000000000..c5a55ceac --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/apolloClient/Queries.tsx @@ -0,0 +1,120 @@ +import React from "react" +import styled from "styled-components" +import { Header } from "reactotron-core-ui" +import { MdWarning } from "react-icons/md" +import { HiDocumentSearch, HiOutlinePencilAlt } from "react-icons/hi" +import { TbDatabaseDollar } from "react-icons/tb" +import { Title } from "../reactNative/components/Shared" + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; +` + +const StorybookContainer = styled.div` + display: flex; + flex-direction: column; + height: 100%; +` + +const TopSection = styled.div` + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +` + +const WarningContainer = styled.div` + display: flex; + color: ${(props) => props.theme.warning}; + background-color: ${(props) => props.theme.backgroundDarker}; + border-top: 1px solid ${(props) => props.theme.chromeLine}; + align-items: center; + padding: 0 20px; +` +const WarningDescription = styled.div` + margin-left: 20px; +` + +function Queries() { + return ( + +
{ + // TODO: Couldn't get react-router-dom to do it for me so I forced it. + window.location.hash = "#/apolloClient/cache" + }, + }, + { + text: "Queries", + icon: HiDocumentSearch, + isActive: true, + + onClick: () => { + // TODO: Couldn't get react-router-dom to do it for me so I forced it. + window.location.hash = "#/apolloClient/queries" + }, + }, + { + text: "Mutations", + icon: HiOutlinePencilAlt, + isActive: false, + onClick: () => { + // TODO: Couldn't get react-router-dom to do it for me so I forced it. + window.location.hash = "#/apolloClient/mutations" + }, + }, + ]} + // actions={[ + // { + // tip: "Search", + // icon: MdSearch, + // onClick: () => { + // toggleSearch() + // }, + // }, + // { + // tip: "Filter", + // icon: MdFilterList, + // onClick: () => { + // openFilter() + // }, + // }, + // { + // tip: "Reverse Order", + // icon: MdSwapVert, + // onClick: () => { + // toggleReverse() + // }, + // }, + // { + // tip: "Clear", + // icon: MdDeleteSweep, + // onClick: () => { + // clearSelectedConnectionCommands() + // }, + // }, + // ]} + /> + + + TODO + + + + This is preview feature. + + + + ) +} + +export default Queries diff --git a/apps/reactotron-app/src/renderer/pages/apolloClient/components/Button.tsx b/apps/reactotron-app/src/renderer/pages/apolloClient/components/Button.tsx new file mode 100644 index 000000000..997208dce --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/apolloClient/components/Button.tsx @@ -0,0 +1,31 @@ +import React from "react" +import styled from "styled-components" + +const Button = styled.button.attrs<{ selected: boolean }>((props) => ({ + selected: props.selected ? props.selected : false, +}))` + height: 30px; + padding: "0 15px"; + font-size: 13px; + margin-right: 4px; + background-color: ${(props) => props.theme.subtleLine}; + border-radius: 2px; + border: 1px solid ${(props) => props.theme.backgroundSubtleDark}; + cursor: pointer; + color: ${(props) => (props.selected ? props.theme.bold : props.theme.foregroundDark)}; +` + +interface OverlayButtonProps { + title: string + selected?: boolean + onClick: React.MouseEventHandler +} + +export function OverlayButton(props: OverlayButtonProps) { + const { selected, title, onClick } = props + return ( + + ) +} diff --git a/lib/reactotron-apollo-client/.babelrc b/lib/reactotron-apollo-client/.babelrc new file mode 100644 index 000000000..c0993b53f --- /dev/null +++ b/lib/reactotron-apollo-client/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "@babel/preset-env", + "@babel/preset-typescript" + ] +} diff --git a/lib/reactotron-apollo-client/.gitignore b/lib/reactotron-apollo-client/.gitignore new file mode 100644 index 000000000..697066da6 --- /dev/null +++ b/lib/reactotron-apollo-client/.gitignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +coverage +.nyc_output +dist +yarn-error.log +.idea diff --git a/lib/reactotron-apollo-client/CHANGELOG.md b/lib/reactotron-apollo-client/CHANGELOG.md new file mode 100644 index 000000000..48dde279a --- /dev/null +++ b/lib/reactotron-apollo-client/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). + +### [0.1.2](https://github.com/infinitered/reactotron/compare/reactotron-@apollo/client@0.1.1...reactotron-@apollo/client@0.1.2) (2023-07-06) + +### [0.1.1](https://github.com/infinitered/reactotron/compare/reactotron-@apollo/client@0.1.0...reactotron-@apollo/client@0.1.1) (2023-06-15) + +## 0.1.0 (2023-06-15) + + +### Features + +* **lib:** add `@apollo/client` plugin ([#1291](https://github.com/infinitered/reactotron/issues/1291)) ([a5637fa](https://github.com/infinitered/reactotron/commit/a5637fae2a3eabcea27cc491d13d0174c46be9e9)) diff --git a/lib/reactotron-apollo-client/LICENSE b/lib/reactotron-apollo-client/LICENSE new file mode 100644 index 000000000..7325864d1 --- /dev/null +++ b/lib/reactotron-apollo-client/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 - 3016 Infinite Red LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/reactotron-apollo-client/README.md b/lib/reactotron-apollo-client/README.md new file mode 100644 index 000000000..1edb1e5b4 --- /dev/null +++ b/lib/reactotron-apollo-client/README.md @@ -0,0 +1,40 @@ +# reactotron-apollo-client + +Log updates to your [@apollo/client](https://github.com/mrousavy/@apollo/client) store in the Reactotron timeline. + +# Installing + +```bash +npm i --save-dev reactotron-apollo-client +# or +yarn add -D reactotron-apollo-client +``` + +## Usage + +Create your Apollo Client as you normally would, and then add the `reactotron-apollo-client` plugin:: + +```js +import { ApolloClient, InMemoryCache } from "@apollo/client" + +const cache = new InMemoryCache() +export const client = new ApolloClient({ + uri: "https://api.graphql.guide/graphql", + cache, + defaultOptions: { watchQuery: { fetchPolicy: "cache-and-network" } }, +}) +``` + +To use the `apolloPlugin`, add the additional plugin on the `import` line. + +```js +import Reactotron from "reactotron-react-native" +import apolloPlugin from "reactotron-apollo-client" +import { client } from "./apolloClient/location" // <--- update this location +... +Reactotron.configure() + .use(apolloPlugin({ apolloClient: client })) // <--- here we go! + .connect() +``` + +And you're done! Now you can see your Apollo caches, queries, and mutations in Reactotron. diff --git a/lib/reactotron-apollo-client/package.json b/lib/reactotron-apollo-client/package.json new file mode 100644 index 000000000..3e79a63a6 --- /dev/null +++ b/lib/reactotron-apollo-client/package.json @@ -0,0 +1,89 @@ +{ + "name": "reactotron-apollo-client", + "version": "0.0.1", + "description": "A Reactotron plugin for @apollo/client.", + "author": "Infinite Red", + "license": "MIT", + "bugs": { + "url": "https://github.com/infinitered/reactotron/issues" + }, + "homepage": "https://github.com/infinitered/reactotron/tree/master/lib/reactotron-apollo-client", + "repository": "https://github.com/infinitered/reactotron/tree/master/lib/reactotron-apollo-client", + "files": [ + "dist", + "src" + ], + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/types/src/index.d.ts", + "react-native": "src/index.ts", + "exports": { + "import": "./dist/index.esm.js", + "types": "./dist/types/src/index.d.ts", + "default": "./dist/index.js" + }, + "scripts": { + "test": "jest --passWithNoTests", + "test:watch": "jest --watch --notify", + "format": "prettier '*.{js,ts,tsx,json,md,css,yml}|**/*.{js,ts,tsx,json,md,css,yml}' --config ../../.prettierrc --ignore-path ../../.prettierignore", + "format:check": "yarn format --check", + "format:write": "yarn format --write", + "prebuild": "yarn clean", + "build": "yarn tsc && yarn compile", + "prebuild:dev": "yarn clean", + "build:dev": "yarn tsc && yarn compile:dev", + "clean": "rimraf ./dist", + "lint": "eslint 'src/**/**.{ts,tsx}'", + "compile": "NODE_ENV=production rollup -c rollup.config.ts", + "compile:dev": "NODE_ENV=development rollup -c rollup.config.ts", + "tsc": "tsc", + "typecheck": "tsc", + "ci:test": "yarn test --runInBand" + }, + "peerDependencies": { + "@apollo/client": "*", + "reactotron-core-client": "*" + }, + "dependencies": { + "ramda": "^0.25.0" + }, + "devDependencies": { + "@apollo/client": "^3.8.3", + "@babel/core": "^7.23.2", + "@babel/eslint-parser": "^7.23.10", + "@babel/preset-typescript": "^7.23.2", + "@types/jest": "^29.5.12", + "@types/node": "^18.18.8", + "@types/ramda": "^0.25.50", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", + "babel-eslint": "^10.1.0", + "babel-jest": "^29.4.3", + "eslint": "^8.35.0", + "eslint-config-prettier": "^9.0.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-n": "^16.2.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-standard": "^5.0.0", + "graphql": "^16.8.0", + "jest": "^29.7.0", + "prettier": "^3.0.3", + "reactotron-core-client": "workspace:*", + "rollup": "^1.1.2", + "rollup-plugin-babel": "^4.4.0", + "rollup-plugin-babel-minify": "^7.0.0", + "rollup-plugin-filesize": "^6.0.1", + "rollup-plugin-node-resolve": "^4.0.0", + "rollup-plugin-resolve": "^0.0.1-predev.1", + "rollup-plugin-terser": "^7.0.2", + "testdouble": "^3.20.0", + "ts-jest": "^29.1.1", + "typescript": "^4.9.5" + }, + "eslintConfig": { + "root": false + } +} diff --git a/lib/reactotron-apollo-client/project.json b/lib/reactotron-apollo-client/project.json new file mode 100644 index 000000000..dffa26c03 --- /dev/null +++ b/lib/reactotron-apollo-client/project.json @@ -0,0 +1,8 @@ +{ + "name": "reactotron-apollo-client", + "targets": { + "version": { + "executor": "@jscutlery/semver:version" + } + } +} diff --git a/lib/reactotron-apollo-client/rollup.config.ts b/lib/reactotron-apollo-client/rollup.config.ts new file mode 100644 index 000000000..2d801ad03 --- /dev/null +++ b/lib/reactotron-apollo-client/rollup.config.ts @@ -0,0 +1,27 @@ +import resolve from "rollup-plugin-node-resolve" +import babel from "rollup-plugin-babel" +import filesize from "rollup-plugin-filesize" +import { terser } from "rollup-plugin-terser" // use terser for minification + +const pkg = require("./package.json") + +export default { + input: "src/index.ts", + output: [ + { + file: pkg.main, + format: "commonjs", + }, + { + file: pkg.module, + format: "esm", + }, + ], + plugins: [ + resolve({ extensions: [".ts"] }), + babel({ extensions: [".ts"], runtimeHelpers: true }), + process.env.NODE_ENV === "production" ? terser() : null, + filesize(), + ], + external: ["ramda", "@apollo/client", "reactotron-core-client", "graphql"], +} diff --git a/lib/reactotron-apollo-client/src/helpers/pathObject.ts b/lib/reactotron-apollo-client/src/helpers/pathObject.ts new file mode 100644 index 000000000..d998d6074 --- /dev/null +++ b/lib/reactotron-apollo-client/src/helpers/pathObject.ts @@ -0,0 +1,19 @@ +export default function pathObject(path: string, obj: any) { + if (!path) return obj + + const splitPaths = path.split(".") + + let pathedObj = obj + + for (let i = 0; i < splitPaths.length; i++) { + const curPath = splitPaths[i] + pathedObj = pathedObj[curPath] + + if (i < splitPaths.length - 1 && typeof pathedObj !== "object") { + pathedObj = undefined + break + } + } + + return pathedObj +} diff --git a/lib/reactotron-apollo-client/src/index.ts b/lib/reactotron-apollo-client/src/index.ts new file mode 100644 index 000000000..6a473801c --- /dev/null +++ b/lib/reactotron-apollo-client/src/index.ts @@ -0,0 +1,3 @@ +import apolloClient from "./reactotron-apollo-client" +export * from "./reactotron-apollo-client" +export default apolloClient diff --git a/lib/reactotron-apollo-client/src/reactotron-apollo-client.ts b/lib/reactotron-apollo-client/src/reactotron-apollo-client.ts new file mode 100644 index 000000000..6b8320f9d --- /dev/null +++ b/lib/reactotron-apollo-client/src/reactotron-apollo-client.ts @@ -0,0 +1,389 @@ +/* eslint-disable no-invalid-this */ +/* eslint-disable func-style */ +import { ApolloClient, ObservableQuery } from "@apollo/client" +import { + ReactotronCore, + Plugin, + assertHasLoggerPlugin, + InferFeatures, + LoggerPlugin, + assertHasStateResponsePlugin, + StateResponsePlugin, +} from "reactotron-core-client" +import type { Command } from "reactotron-core-contract" +import type { DocumentNode, NormalizedCacheObject } from "@apollo/client" +import { getOperationName } from "@apollo/client/utilities" +import type { QueryInfo } from "@apollo/client/core/QueryInfo" + +import type { ASTNode } from "graphql" +import { print } from "graphql" + +// import { flatten, uniq } from "ramda" +// import pathObject from "./helpers/pathObject" + +type ApolloClientType = ApolloClient + +type Variables = QueryInfo["variables"] + +type RawQueryData = { + document: ASTNode + variables: Variables + observableQuery: ObservableQuery + lastDiff: any + diff: any + queryId: string +} + +type QueryData = { + id: string + queryString: string + variables: Variables + cachedData: string + name: string | undefined +} + +type MutationData = { + id: string + name: string | null + variables: object + loading: boolean + error: object + body: string | undefined +} + +// type Callback = () => any + +type ArrayOfQuery = Array +type ArrayOfMutations = Array + +type ApolloClientState = { + id: number + lastUpdateAt: string + queries: ArrayOfQuery + mutations: ArrayOfMutations + cache: object +} + +// TODO utilize when we do Queries and Mutations +// type RawMutationBody = { +// id: string +// name: string | null +// body: string +// variables: object +// } + +// type RawQueryBody = { +// id: string +// name: string | null +// cachedData: object +// } + +// type RawData = { +// id: string +// lastUpdateAt: Date +// queries: ArrayOfQuery +// mutations: ArrayOfMutations +// cache: Array +// } + +// type Data = { +// id: string +// lastUpdateAt: Date +// queries: Array +// mutations: Array +// cache: Array +// } + +// type BlockType = { +// id?: string +// operationType?: string +// name?: string | null +// blocks?: Array<{ +// blockType: string +// blockLabel: string +// blockValue: any +// }> +// } + +let tick = 0 + +function getTime(): string { + const date = new Date() + return `${date.getHours()}:${date.getMinutes()}` +} + +function extractQueries(client: ApolloClientType): Map { + // @ts-expect-error queryManager is private method + if (!client || !client.queryManager) { + return new Map() + } + // @ts-expect-error queryManager is private method + return client.queryManager.queries +} + +function getQueries(queryMap: Map): ArrayOfQuery { + const queries: ArrayOfQuery = [] + + if (queryMap) { + // @ts-expect-error Type 'IterableIterator' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher + ;[...queryMap.values()].forEach( + ({ document, variables, observableQuery, diff, lastDiff }, queryId) => { + if (document && observableQuery) { + queries.push({ + queryString: print(document), + variables, + cachedData: diff?.result || lastDiff?.diff?.result, + name: observableQuery?.queryName, + id: queryId?.toString(), + }) + } + } + ) + } + return queries +} + +function getAllQueries(client: ApolloClientType): ArrayOfQuery { + const queryMap = extractQueries(client) + const allQueries = getQueries(queryMap) + return allQueries +} + +type MutationObject = { + mutation: DocumentNode + variables: object + loading: boolean + error: object +} +function getMutationData(allMutations: Record): Array { + return [...Object.keys(allMutations)]?.map((key) => { + const { mutation, variables, loading, error } = allMutations[key] + + return { + id: key, + name: getOperationName(mutation), + variables, + loading, + error, + body: mutation?.loc?.source?.body, + } + }) +} + +function getAllMutations(client: ApolloClientType): ArrayOfMutations { + // @ts-expect-error private method + const allMutations = client.queryManager.mutationStore || {} + + const final = getMutationData(allMutations) + + return final +} + +function getCurrentState(client: ApolloClientType): Promise { + tick++ + + let currentState: ApolloClientState + + return new Promise((resolve) => { + setTimeout(() => { + currentState = { + id: tick, + lastUpdateAt: getTime(), + queries: getAllQueries(client), + mutations: getAllMutations(client), + cache: client.cache.extract(true), + } + resolve(currentState) + }, 0) + }).then(() => { + return currentState + }) +} + +// function debounce(func: (...args: any) => any, timeout = 500): () => any { +// let timer +// return (...args) => { +// clearTimeout(timer) +// timer = setTimeout(() => { +// // @ts-expect-error add typings for this +// func.apply(this, args) +// }, timeout) +// } +// } + +export interface ApolloPluginConfig { + apolloClient: ApolloClient +} + +export default function apolloPlugin(options: ApolloPluginConfig) { + return (reactotronClient: Client) => { + const { apolloClient } = options + assertHasLoggerPlugin(reactotronClient) + assertHasStateResponsePlugin(reactotronClient) + const reactotron = reactotronClient as Client & + InferFeatures & + InferFeatures + + // --- Plugin-scoped variables --------------------------------- + // let acknowledged = true + let apolloData: null | ApolloClientState + + // hang on to the apollo state + function setup() { + getCurrentState(apolloClient).then((data) => { + apolloData = data + }) + + // function sendData() { + // reactotron.log("sendData") + // if (apolloData) { + // acknowledged = false + // } + // } + + // const poll = async (): Promise => { + // // TODO remove + // reactotron.display({ + // name: "APOLLO CLIENT", + // preview: `Poll`, + // value: { acknowledged }, + // }) + + // if (acknowledged) { + // getCurrentState(apolloClient).then((data) => { + // apolloData = data + // }) + // reactotron.log("apolloData", apolloData) + // sendData() + // } + // // sendSubscriptions() + // } + } + + // a list of subscriptions the client is subscribing to + // let subscriptions: string[] = [] + + function subscribe(command: Command<"state.values.subscribe">) { + const paths: string[] = (command && command.payload && command.payload.paths) || [] + + if (paths) { + // TODO ditch ramda + // subscriptions = uniq(flatten(paths)) + } + + // sendSubscriptions() + } + + // function ack(command: Command<"ack">) { + // reactotron.log("ack", command) + // acknowledged = true + // } + + // function getChanges() { + // // TODO also check if cache state is empty + // if (!reactotron) return [] + + // reactotron.log("subscriptions", subscriptions) + + // const changes = [] + + // const state = apolloData.cache + // reactotron.log("getChanges", state) + + // subscriptions.forEach((path) => { + // let cleanedPath = path + // let starredPath = false + + // if (path && path.endsWith("*")) { + // // Handle the star! + // starredPath = true + // cleanedPath = path.substring(0, path.length - 2) + // } + + // const values = pathObject(cleanedPath, state) + + // if (starredPath && cleanedPath && values) { + // changes.push( + // ...Object.entries(values).map((val) => ({ + // path: `${cleanedPath}.${val[0]}`, + // value: val[1], + // })) + // ) + // } else { + // changes.push({ path: cleanedPath, value: state[cleanedPath] }) + // } + // }) + + // return changes + // } + + // function sendSubscriptions() { + // const changes = getChanges() + // reactotron.stateValuesChange(changes) + // } + + async function handleRequest() { + // @ts-expect-error fix command type payload + reactotron.send("apollo.response", await getCurrentState(apolloClient)) + } + + async function handleUpdateCache(command: Command<"apollo.cache.update">) { + const { typename, identifier, fieldName, fieldValue } = command.payload + const result = apolloClient.cache.modify({ + // id: apolloClient.cache.identify({ __typename: typename, [keyField]: keyValue }), + id: apolloClient.cache.identify({ __typename: typename, ...identifier }), + fields: { + [fieldName]() { + // newValue is received from the WebSocket message + return fieldValue // Update the dynamically specified field + }, + }, + }) + + reactotron.log("apollo.cache.update", result, identifier) + } + + async function handleAck() { + // acknowledged = true + // const data = await getCurrentState(apolloClient) + if (apolloData) { + // @ts-expect-error fix command type payload + reactotron.send("apollo.response", apolloData) + // acknowledged = false + apolloData = null + } + } + + // --- Reactotron Hooks --------------------------------- + + // maps inbound commands to functions to run + // TODO clear cache command? + const COMMAND_MAP = { + "state.values.subscribe": subscribe, + "apollo.ack": handleAck, + "apollo.request": handleRequest, + "apollo.cache.update": handleUpdateCache, + } satisfies { [name: string]: (command: Command) => void } + + /** + * Fires when we receive a command from the reactotron app. + */ + function onCommand(command: Command) { + // lookup the command and execute + const handler = COMMAND_MAP[command && command.type] + handler && handler(command) + } + + // --- Reactotron plugin interface --------------------------------- + + return { + name: "apollo-client", + // Fires when we receive a command from the Reactotron app. + onCommand, + + onConnect() { + setup() + }, + } satisfies Plugin + } +} diff --git a/lib/reactotron-apollo-client/tsconfig.json b/lib/reactotron-apollo-client/tsconfig.json new file mode 100644 index 000000000..1052dea71 --- /dev/null +++ b/lib/reactotron-apollo-client/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": false, + "declaration": true, + "declarationDir": "dist/types", + "rootDir": ".", + "emitDeclarationOnly": true, + "emitDecoratorMetadata": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "module": "es2015", + "moduleResolution": "node", + "noImplicitAny": false, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "sourceMap": true, + "target": "es5", + "esModuleInterop": true, + }, + "exclude": ["node_modules"], + "include": ["src", "test"], +} diff --git a/lib/reactotron-core-client/src/plugins/api-response.ts b/lib/reactotron-core-client/src/plugins/api-response.ts index 8ada9cd06..a2d677b20 100644 --- a/lib/reactotron-core-client/src/plugins/api-response.ts +++ b/lib/reactotron-core-client/src/plugins/api-response.ts @@ -5,6 +5,7 @@ import type { ReactotronCore, Plugin } from "../reactotron-core-client" */ const apiResponse = () => (reactotron: ReactotronCore) => { return { + name: "api-response", features: { apiResponse: (request: { status: number }, response: any, duration: number) => { const ok = diff --git a/lib/reactotron-core-client/src/plugins/benchmark.ts b/lib/reactotron-core-client/src/plugins/benchmark.ts index 4d53bd9ba..c8d42828c 100644 --- a/lib/reactotron-core-client/src/plugins/benchmark.ts +++ b/lib/reactotron-core-client/src/plugins/benchmark.ts @@ -23,6 +23,7 @@ const benchmark = () => (reactotron: ReactotronCore) => { } return { + name: "benchmark", features: { benchmark }, } satisfies Plugin } diff --git a/lib/reactotron-core-client/src/plugins/clear.ts b/lib/reactotron-core-client/src/plugins/clear.ts index 54d049941..72a627afc 100644 --- a/lib/reactotron-core-client/src/plugins/clear.ts +++ b/lib/reactotron-core-client/src/plugins/clear.ts @@ -5,6 +5,7 @@ import type { ReactotronCore, Plugin } from "../reactotron-core-client" */ const clear = () => (reactotron: ReactotronCore) => { return { + name: "clear", features: { clear: () => reactotron.send("clear"), }, diff --git a/lib/reactotron-core-client/src/plugins/image.ts b/lib/reactotron-core-client/src/plugins/image.ts index 0b886a8c9..89f03e198 100644 --- a/lib/reactotron-core-client/src/plugins/image.ts +++ b/lib/reactotron-core-client/src/plugins/image.ts @@ -14,6 +14,7 @@ export interface ImagePayload { */ const image = () => (reactotron: ReactotronCore) => { return { + name: "image", features: { // expanded just to show the specs image: (payload: ImagePayload) => { diff --git a/lib/reactotron-core-client/src/plugins/logger.ts b/lib/reactotron-core-client/src/plugins/logger.ts index a1056543f..1e7f08ad0 100644 --- a/lib/reactotron-core-client/src/plugins/logger.ts +++ b/lib/reactotron-core-client/src/plugins/logger.ts @@ -5,6 +5,7 @@ import type { ReactotronCore, Plugin, InferFeatures } from "../reactotron-core-c */ const logger = () => (reactotron: ReactotronCore) => { return { + name: "logger", features: { log: (...args) => { const content = args && args.length === 1 ? args[0] : args diff --git a/lib/reactotron-core-client/src/plugins/repl.ts b/lib/reactotron-core-client/src/plugins/repl.ts index 61278b347..bc5095d5f 100644 --- a/lib/reactotron-core-client/src/plugins/repl.ts +++ b/lib/reactotron-core-client/src/plugins/repl.ts @@ -7,6 +7,7 @@ const repl = () => (reactotron: ReactotronCore) => { const myRepls: { [key: string]: AcceptableRepls } = {} // let currentContext = null return { + name: "repl", onCommand: ({ type, payload }) => { if (type.substr(0, 5) !== "repl.") return diff --git a/lib/reactotron-core-client/src/plugins/state-responses.ts b/lib/reactotron-core-client/src/plugins/state-responses.ts index d7256c3b7..221e4a6b8 100644 --- a/lib/reactotron-core-client/src/plugins/state-responses.ts +++ b/lib/reactotron-core-client/src/plugins/state-responses.ts @@ -12,6 +12,7 @@ import type { ReactotronCore, Plugin, InferFeatures } from "../reactotron-core-c */ const stateResponse = () => (reactotron: ReactotronCore) => { return { + name: "state-response", features: { stateActionComplete: ( name: StateActionCompletePayload["name"], diff --git a/lib/reactotron-core-client/src/reactotron-core-client.ts b/lib/reactotron-core-client/src/reactotron-core-client.ts index 792e197d3..c277ece3f 100644 --- a/lib/reactotron-core-client/src/reactotron-core-client.ts +++ b/lib/reactotron-core-client/src/reactotron-core-client.ts @@ -36,6 +36,7 @@ export interface LifeCycleMethods { type AnyFunction = (...args: any[]) => any export interface Plugin extends LifeCycleMethods { + name: string features?: { [key: string]: AnyFunction } @@ -294,6 +295,7 @@ export class ReactotronImpl name, clientId, reactotronCoreClientVersion: "REACTOTRON_CORE_CLIENT_VERSION", + plugins: this.plugins.map((f) => f.name).filter((s) => Boolean(s)), }) // flush the send queue diff --git a/lib/reactotron-core-client/test/plugin-features.test.ts b/lib/reactotron-core-client/test/plugin-features.test.ts index 986e4d883..93bea698d 100644 --- a/lib/reactotron-core-client/test/plugin-features.test.ts +++ b/lib/reactotron-core-client/test/plugin-features.test.ts @@ -14,7 +14,7 @@ test("features must be an object if they appear", () => { test("some names are not allowed", () => { const client = createClient({ createSocket }) - const createPlugin = (features) => () => ({ features }) + const createPlugin = (features) => () => ({ name: "test-plugin", features }) const badPlugins = [ "options", @@ -38,7 +38,7 @@ test("features can be added and called", () => { const features = { magic: () => 42, } - return { features } + return { name: "test-plugin", features } } const client = createClient({ createSocket }).use(plugin()) expect(typeof client.magic).toBe("function") @@ -46,7 +46,10 @@ test("features can be added and called", () => { }) test("you can overwrite other feature names", () => { - const createPlugin = (number) => () => ({ features: { hello: () => number } }) + const createPlugin = (number) => () => ({ + name: "test-plugin", + features: { hello: () => number }, + }) const client = createClient({ createSocket }).use(createPlugin(69)) expect(client.hello()).toBe(69) expect(client.use(createPlugin(9001)).hello()).toBe(9001) diff --git a/lib/reactotron-core-client/test/plugin-interface.test.ts b/lib/reactotron-core-client/test/plugin-interface.test.ts index 2d7a08afb..6e952122a 100644 --- a/lib/reactotron-core-client/test/plugin-interface.test.ts +++ b/lib/reactotron-core-client/test/plugin-interface.test.ts @@ -39,16 +39,17 @@ test("plugins are invoke and return an object", () => { expect(() => client.use(() => "")).toThrow() // @ts-ignore should be ts-expect-error but jest doesn't like it for null or undefined values for some reason expect(() => client.use(() => undefined)).toThrow() - client.use(() => ({})) + client.use(() => ({ name: "empty" })) }) test("plugins can literally do nothing", () => { - const empty = () => ({}) + const empty = () => ({ name: "empty" }) client.use(empty) expect(client.plugins.length).toBe(corePlugins.length + 1) }) test("initialized with the config object", (done) => { + // @ts-expect-error ignore name property client.use((reactotron) => { expect(typeof reactotron).toBe("object") expect(reactotron).toBe(client) @@ -60,7 +61,10 @@ test("initialized with the config object", (done) => { }) test("can be added in createClient", () => { - const createPlugin = (name, value) => () => ({ features: { [name]: () => value } }) + const createPlugin = (name, value) => () => ({ + name: `plugin-${name}`, + features: { [name]: () => value }, + }) const clientWithPlugins = createClient< ReactotronCore & { sayHello: () => void; sayGoodbye: () => void } >({ diff --git a/lib/reactotron-core-client/test/plugin-on-connect.test.ts b/lib/reactotron-core-client/test/plugin-on-connect.test.ts index 3fbaafbba..e8a24f09f 100644 --- a/lib/reactotron-core-client/test/plugin-on-connect.test.ts +++ b/lib/reactotron-core-client/test/plugin-on-connect.test.ts @@ -22,6 +22,7 @@ test("plugins support onConnect", (done) => { onConnect: () => { done() }, + name: "test-plugin", }) // create a client & add the plugin diff --git a/lib/reactotron-core-client/test/plugin-on-disconnect.test.ts b/lib/reactotron-core-client/test/plugin-on-disconnect.test.ts index 736e9984b..ad103f165 100644 --- a/lib/reactotron-core-client/test/plugin-on-disconnect.test.ts +++ b/lib/reactotron-core-client/test/plugin-on-disconnect.test.ts @@ -14,7 +14,8 @@ test("plugins support onDisconnect", (done) => { createClosingServer(port) // this plugin supports onDisconnect - const plugin = () => ({ onDisconnect: done }) satisfies Plugin + const plugin = () => + ({ name: "test-plugin", onDisconnect: done }) satisfies Plugin // create a client & add the plugin createClient({ createSocket, port, plugins: [plugin] }).connect() diff --git a/lib/reactotron-core-client/test/plugin-on-plugin.test.ts b/lib/reactotron-core-client/test/plugin-on-plugin.test.ts index 890029388..f99ca2350 100644 --- a/lib/reactotron-core-client/test/plugin-on-plugin.test.ts +++ b/lib/reactotron-core-client/test/plugin-on-plugin.test.ts @@ -13,6 +13,7 @@ test("plugins support onPlugin", (done) => { expect(instance).toBe(client) done() }, + name: "test-plugin", }) // add the plugin diff --git a/lib/reactotron-core-client/test/plugin-send.test.ts b/lib/reactotron-core-client/test/plugin-send.test.ts index d27d361d4..c36e87dde 100644 --- a/lib/reactotron-core-client/test/plugin-send.test.ts +++ b/lib/reactotron-core-client/test/plugin-send.test.ts @@ -24,7 +24,9 @@ test("plugins support send", (done) => { // the plugin to extract the send function const plugin: PluginCreator = (reactotron) => { capturedSend = reactotron.send - return {} + return { + name: "test-plugin", + } } // create the client, add the plugin, and connect diff --git a/lib/reactotron-core-contract/src/command.ts b/lib/reactotron-core-contract/src/command.ts index 999291caa..87e921723 100644 --- a/lib/reactotron-core-contract/src/command.ts +++ b/lib/reactotron-core-contract/src/command.ts @@ -1,6 +1,7 @@ import type { LogPayload } from "./log" import { EditorOpenPayload } from "./openInEditor" import type { + ApolloClientCacheUpdatePayload, StateActionCompletePayload, StateActionDispatchPayload, StateBackupRequestPayload, @@ -45,6 +46,10 @@ export const CommandType = { EditorOpen: "editor.open", Storybook: "storybook", Overlay: "overlay", + ApolloClientRequest: "apollo.request", + ApolloClientResponse: "apollo.response", + ApolloClientAck: "apollo.ack", + ApolloClientUpdateCache: "apollo.cache.update", } as const export type CommandTypeKey = (typeof CommandType)[keyof typeof CommandType] @@ -79,6 +84,10 @@ export interface CommandMap { [CommandType.EditorOpen]: EditorOpenPayload [CommandType.Storybook]: boolean [CommandType.Overlay]: boolean + [CommandType.ApolloClientAck]: boolean + [CommandType.ApolloClientRequest]: { message: string } + [CommandType.ApolloClientResponse]: { message: string } + [CommandType.ApolloClientUpdateCache]: ApolloClientCacheUpdatePayload } export interface Command< diff --git a/lib/reactotron-core-contract/src/state.ts b/lib/reactotron-core-contract/src/state.ts index f49acd947..64008fc31 100644 --- a/lib/reactotron-core-contract/src/state.ts +++ b/lib/reactotron-core-contract/src/state.ts @@ -60,3 +60,12 @@ export interface StateActionCompletePayload { action: Record ms?: number } + +export interface ApolloClientCacheUpdatePayload { + typename: string + identifier: Record + // keyField: string + // keyValue: Value + fieldName: string + fieldValue: Value +} diff --git a/lib/reactotron-core-ui/src/components/TreeView/index.tsx b/lib/reactotron-core-ui/src/components/TreeView/index.tsx index 19fd365e7..0f3d62f0a 100644 --- a/lib/reactotron-core-ui/src/components/TreeView/index.tsx +++ b/lib/reactotron-core-ui/src/components/TreeView/index.tsx @@ -1,5 +1,6 @@ import React from "react" import { JSONTree } from "react-json-tree" +import type { ValueRenderer } from "react-json-tree" import styled from "styled-components" import useColorScheme from "../../hooks/useColorScheme" @@ -31,6 +32,8 @@ const MutedContainer = styled.span` color: ${(props) => props.theme.highlight}; ` +const SpanContainer = styled.span`` + const getTreeTheme = (baseTheme: ReactotronTheme) => ({ tree: { backgroundColor: "transparent", marginTop: -3 }, ...theme, @@ -41,31 +44,40 @@ interface Props { // value: object value: any level?: number + valueRenderer?: ValueRenderer + expand?: boolean } -export default function TreeView({ value, level = 1 }: Props) { +export default function TreeView({ value, valueRenderer, level = 1, expand = false }: Props) { const colorScheme = useColorScheme() + const renderer = (transformed: any, untransformed: any, ...keyPath: any) => { + if (valueRenderer) { + return valueRenderer(transformed, untransformed, ...keyPath) + } + + return {`${untransformed || transformed}`} + } return ( minLevel <= level} + shouldExpandNodeInitially={(keyName, data, minLevel) => expand || minLevel <= level} theme={getTreeTheme(themes[colorScheme])} getItemString={(type, data, itemType, itemString) => { + // when it's an object, display {} if (type === "Object") { return {itemType} } + // when it's an array, display [] X items return ( {itemType} {itemString} ) }} - valueRenderer={(transformed, untransformed) => { - return {`${untransformed || transformed}`} - }} + valueRenderer={renderer} /> ) } diff --git a/lib/reactotron-core-ui/src/contexts/ApolloClient/index.tsx b/lib/reactotron-core-ui/src/contexts/ApolloClient/index.tsx new file mode 100644 index 000000000..baad5ff88 --- /dev/null +++ b/lib/reactotron-core-ui/src/contexts/ApolloClient/index.tsx @@ -0,0 +1,177 @@ +import React, { FunctionComponent } from "react" + +import useApolloClient, { ApolloClientData, INITIAL_DATA } from "./useApolloClient" +import ReactotronContext from "../Reactotron" +import { CommandType } from "reactotron-core-contract" + +interface Context { + isSearchOpen: boolean + toggleSearch: () => void + openSearch: () => void + closeSearch: () => void + search: string + setSearch: (search: string) => void + viewedKeys: string[] + setViewedKeys: (viewedKey: string) => void + currentIndex: number + setCurrentIndex: (currentIndex: number) => void + getCurrentKey: () => string | null + goForward: () => void + goBack: () => void + togglePin: (key: string) => void + pinnedKeys: string[] + data: ApolloClientData + setData: (data: ApolloClientData) => void + isEditOpen: boolean + openEdit: () => void + closeEdit: () => void +} + +const ApolloClientContext = React.createContext({ + isSearchOpen: false, + toggleSearch: null, + openSearch: null, + closeSearch: null, + search: "", + setSearch: null, + viewedKeys: [], + setViewedKeys: null, + currentIndex: -1, + setCurrentIndex: null, + getCurrentKey: null, + goForward: null, + goBack: null, + togglePin: null, + pinnedKeys: [], + data: INITIAL_DATA, + setData: null, + isEditOpen: false, + openEdit: null, + closeEdit: null, +}) + +const Provider: FunctionComponent = ({ children }) => { + const { + isSearchOpen, + toggleSearch, + openSearch, + closeSearch, + search, + setSearch, + setViewedKeys, + viewedKeys, + currentIndex, + setCurrentIndex, + getCurrentKey, + goBack, + goForward, + togglePin, + pinnedKeys, + data, + setData, + isEditOpen, + openEdit, + closeEdit, + } = useApolloClient() + const { sendCommand, addCommandListener } = React.useContext(ReactotronContext) + const lastQueryKeys = React.useRef | null>(null) + + // send polling apollo.request command every half second + React.useEffect(() => { + const interval = setInterval(() => { + sendCommand("apollo.request", {}) + }, 1000) + return () => clearInterval(interval) + }, [sendCommand]) + + React.useEffect(() => { + addCommandListener((command) => { + if (command.type === CommandType.ApolloClientResponse) { + // TODO diff the payload for new queries by name, maybe log to timeline? + const newQueries = diffAndLogNewQueries(lastQueryKeys.current, command.payload.queries) + console.log({ newQueries }) + if (newQueries) { + newQueries.forEach((queryIndex) => { + const query = command.payload.queries[queryIndex] + sendCommand(CommandType.Display, { + name: "Apollo Client", + preview: `Query: ${query.name}`, + value: { variables: query.variables, queryString: query.queryString }, + }) + }) + lastQueryKeys.current = new Set(newQueries) + } + // console.log({ newQueries, allQueries: command.payload.queries }) + + // TODO diff for subscriptions + + // TODO reorder the way things come in so recent is at top ? + console.log(command.payload) + setData(command.payload) + sendCommand("apollo.ack", {}) + } + }) + }, [addCommandListener, sendCommand, setData]) + + return ( + + {children} + + ) +} + +export default ApolloClientContext +export const ApolloClientProvider = Provider + +function diffAndLogNewQueries(lastQueryKeys: Set | null, currentQueries: Map) { + const currentQueryKeys = new Set(currentQueries.keys()) + + const newQueries = [] + if (lastQueryKeys) { + // Determine new queries by comparing keys + currentQueryKeys.forEach((key) => { + if (!lastQueryKeys.has(key)) { + newQueries.push(key) + } + }) + + // Log new queries + if (newQueries.length > 0) { + console.log("New Queries:", newQueries, lastQueryKeys) + // return newQueries + } else { + console.log("No new queries since last check.") + } + } else { + console.log("Initial query tracking setup.") + currentQueryKeys.forEach((key) => { + newQueries.push(key) + }) + } + + // Update the last known set of query keys + return newQueries +} diff --git a/lib/reactotron-core-ui/src/contexts/ApolloClient/useApolloClient.ts b/lib/reactotron-core-ui/src/contexts/ApolloClient/useApolloClient.ts new file mode 100644 index 000000000..5bec928af --- /dev/null +++ b/lib/reactotron-core-ui/src/contexts/ApolloClient/useApolloClient.ts @@ -0,0 +1,246 @@ +import { useCallback, useReducer } from "react" + +// export enum StorageKey { +// ReversedOrder = "ReactotronApolloClientReversedOrder", +// HiddenCommands = "ReactotronApolloClientHiddenCommands", +// } + +export interface ApolloClientData { + id: string + lastUpdatedAt: Date + // cache array of objects + cache: Record +} + +interface ApolloClientState { + isSearchOpen: boolean + search: string + viewedKeys: string[] + currentIndex: number + pinnedKeys: string[] + data: ApolloClientData + isEditOpen: boolean +} + +export const INITIAL_DATA = { + id: "x", + lastUpdatedAt: new Date(), + // queries: [], + // mutations: [], + cache: {}, +} + +enum ApolloClientActionType { + SearchOpen = "SEARCH_OPEN", + SearchClose = "SEARCH_CLOSE", + SearchSet = "SEARCH_SET", + ViewedKeysSet = "VIEWED_KEYS_SET", + IndexSet = "INDEX_SET", + PinnedKeysSet = "PINNED_KEYS_SET", + DataSet = "DATA_SET", + EditOpen = "EDIT_OPEN", + EditClose = "EDIT_CLOSE", +} + +type Action = + | { + type: + | ApolloClientActionType.SearchOpen + | ApolloClientActionType.SearchClose + | ApolloClientActionType.EditOpen + | ApolloClientActionType.EditClose + } + | { + type: ApolloClientActionType.SearchSet + payload: string + } + | { + type: ApolloClientActionType.PinnedKeysSet + payload: string[] + } + | { + type: ApolloClientActionType.IndexSet + payload: number + } + | { + type: ApolloClientActionType.ViewedKeysSet + payload: string[] + } + | { + type: ApolloClientActionType.DataSet + payload: ApolloClientData + } + +function ApolloClientReducer(state: ApolloClientState, action: Action) { + switch (action.type) { + case ApolloClientActionType.SearchOpen: + return { ...state, isSearchOpen: true } + case ApolloClientActionType.SearchClose: + return { ...state, isSearchOpen: false } + case ApolloClientActionType.EditClose: + return { ...state, isEditOpen: false } + case ApolloClientActionType.EditOpen: + return { ...state, isEditOpen: true } + case ApolloClientActionType.SearchSet: + return { ...state, search: action.payload } + case ApolloClientActionType.ViewedKeysSet: + return { ...state, viewedKeys: action.payload, currentIndex: action.payload.length - 1 } + case ApolloClientActionType.IndexSet: + return { ...state, currentIndex: action.payload } + case ApolloClientActionType.PinnedKeysSet: + return { ...state, pinnedKeys: action.payload } + case ApolloClientActionType.DataSet: + return { ...state, data: action.payload } + default: + return state + } +} + +function useApolloClient() { + const [state, dispatch] = useReducer(ApolloClientReducer, { + isSearchOpen: false, + search: "", + viewedKeys: [], + currentIndex: -1, + pinnedKeys: [], + data: INITIAL_DATA, + isEditOpen: false, + }) + + // Setup event handlers + const toggleSearch = useCallback(() => { + dispatch({ + type: state.isSearchOpen + ? ApolloClientActionType.SearchClose + : ApolloClientActionType.SearchOpen, + }) + }, [state.isSearchOpen]) + + const openSearch = useCallback(() => { + dispatch({ + type: ApolloClientActionType.SearchOpen, + }) + }, []) + + const closeSearch = useCallback(() => { + dispatch({ + type: ApolloClientActionType.SearchClose, + }) + }, []) + + const openEdit = useCallback(() => { + dispatch({ + type: ApolloClientActionType.EditOpen, + }) + }, []) + + const closeEdit = useCallback(() => { + dispatch({ + type: ApolloClientActionType.EditClose, + }) + }, []) + + const setSearch = useCallback((search: string) => { + dispatch({ + type: ApolloClientActionType.SearchSet, + payload: search, + }) + }, []) + + const goBack = useCallback(() => { + if (state.currentIndex > 0) { + dispatch({ + type: ApolloClientActionType.IndexSet, + payload: state.currentIndex - 1, + }) + } + }, [state.currentIndex]) + + const goForward = useCallback(() => { + if (state.currentIndex < state.viewedKeys.length - 1) { + dispatch({ + type: ApolloClientActionType.IndexSet, + payload: state.currentIndex + 1, + }) + } + }, [state.currentIndex, state.viewedKeys]) + + const setViewedKeys = useCallback( + (viewedKey: string) => { + if ( + state.viewedKeys.length === 0 || + state.viewedKeys[state.viewedKeys.length - 1] !== viewedKey + ) { + const newHistory = [...state.viewedKeys, viewedKey] + dispatch({ + type: ApolloClientActionType.ViewedKeysSet, + payload: newHistory, + }) + + dispatch({ + type: ApolloClientActionType.IndexSet, + payload: newHistory.length - 1, + }) + } + }, + [state.viewedKeys] + ) + + const setCurrentIndex = useCallback((index: number) => { + dispatch({ + type: ApolloClientActionType.IndexSet, + payload: index, + }) + }, []) + + const getCurrentKey = useCallback(() => { + return state.currentIndex >= 0 ? state.viewedKeys[state.currentIndex] : null + }, [state.currentIndex, state.viewedKeys]) + + const togglePin = useCallback( + (key: string) => { + const newPinnedKeys = state.pinnedKeys.includes(key) + ? state.pinnedKeys.filter((k) => k !== key) + : [...state.pinnedKeys, key] + dispatch({ + type: ApolloClientActionType.PinnedKeysSet, + payload: newPinnedKeys, + }) + }, + [state.pinnedKeys] + ) + + const setData = useCallback((data: ApolloClientData) => { + dispatch({ + type: ApolloClientActionType.DataSet, + payload: data, + }) + }, []) + + const contextValue = { + isSearchOpen: state.isSearchOpen, + toggleSearch, + openSearch, + closeSearch, + search: state.search, + setSearch, + viewedKeys: state.viewedKeys, + setViewedKeys, + setCurrentIndex, + currentIndex: state.currentIndex, + getCurrentKey, + goForward, + goBack, + togglePin, + pinnedKeys: state.pinnedKeys, + data: state.data, + setData, + openEdit, + closeEdit, + isEditOpen: state.isEditOpen, + } + + return contextValue +} + +export default useApolloClient diff --git a/lib/reactotron-core-ui/src/index.ts b/lib/reactotron-core-ui/src/index.ts index 4f0806136..7c15c1391 100644 --- a/lib/reactotron-core-ui/src/index.ts +++ b/lib/reactotron-core-ui/src/index.ts @@ -6,6 +6,7 @@ import ActionButton from "./components/ActionButton" import ContentView from "./components/ContentView" import EmptyState from "./components/EmptyState" import Header from "./components/Header" +import Checkbox from "./components/Checkbox" import Modal from "./components/Modal" import RandomJoke from "./components/RandomJoke" import ReactotronAppProvider from "./components/ReactotronAppProvider" @@ -22,8 +23,10 @@ import CustomCommandsContext, { CustomCommandsProvider } from "./contexts/Custom import ReactNativeContext, { ReactNativeProvider } from "./contexts/ReactNative" import StateContext, { StateProvider } from "./contexts/State" import TimelineContext, { TimelineProvider } from "./contexts/Timeline" +import ApolloClientContext, { ApolloClientProvider } from "./contexts/ApolloClient" // Modals +import ApolloUpdateCacheValueModal from "./modals/ApolloUpdateCacheValueModal" import DispatchActionModal from "./modals/DispatchActionModal" import SnapshotRenameModal from "./modals/SnapshotRenameModal" import SubscriptionAddModal from "./modals/SubscriptionAddModal" @@ -37,8 +40,11 @@ import repairSerialization from "./utils/repair-serialization" import filterCommands from "./utils/filterCommands" export { - // Contexts ActionButton, + ApolloClientContext, + ApolloClientProvider, + ApolloUpdateCacheValueModal, + Checkbox, ContentView, CustomCommandsContext, CustomCommandsProvider, diff --git a/lib/reactotron-core-ui/src/modals/ApolloUpdateCacheValueModal/index.tsx b/lib/reactotron-core-ui/src/modals/ApolloUpdateCacheValueModal/index.tsx new file mode 100644 index 000000000..144e0422a --- /dev/null +++ b/lib/reactotron-core-ui/src/modals/ApolloUpdateCacheValueModal/index.tsx @@ -0,0 +1,178 @@ +import React, { FunctionComponent, useRef, useState, useCallback, useEffect } from "react" +import styled from "styled-components" + +import Modal, { KeystrokeContainer, Keystroke } from "../../components/Modal" +import type { ApolloClientCacheUpdatePayload } from "reactotron-core-contract" + +const KEY_MAPS = { + command: "⌘", + ctrl: "CTRL", +} + +const InstructionText = styled.div` + text-align: left; + color: ${(props) => props.theme.foreground}; +` + +const ActionContainer = styled.div` + display: flex; + flex-direction: column; + padding: 15px; +` +const ActionLabel = styled.label` + font-size: 13px; + color: ${(props) => props.theme.heading}; + padding-bottom: 20px; +` +const ActionInput = styled.textarea` + border: 0; + border-bottom: 1px solid ${(props) => props.theme.line}; + font-size: 25px; + color: ${(props) => props.theme.foregroundLight}; + line-height: 40px; + background-color: inherit; + min-width: 462px; + max-width: 462px; + height: 150px; + min-height: 40px; + max-height: 300px; +` + +const ActionNumber = styled.input` + border: 0; + border-bottom: 1px solid ${(props) => props.theme.line}; + font-size: 25px; + color: ${(props) => props.theme.foregroundLight}; + line-height: 40px; + background-color: inherit; + min-width: 462px; + max-width: 462px; +` + +const ActionCheckbox = styled.input` + border: 0; + border-bottom: 1px solid ${(props) => props.theme.line}; + font-size: 25px; + color: ${(props) => props.theme.foregroundLight}; + line-height: 40px; + background-color: inherit; + height: 40px; + width: 40px; +` + +const isDarwin = process.platform === "darwin" + +interface Props { + isOpen: boolean + initialValue?: ApolloClientCacheUpdatePayload + onClose: () => void + onDispatchAction: (updates: ApolloClientCacheUpdatePayload) => void + cacheKey: string +} + +const ApolloUpdateCacheValueModal: FunctionComponent = ({ + isOpen, + initialValue, + onClose, + onDispatchAction, + cacheKey, +}) => { + const [prevIsOpen, setPrevIsOpen] = useState(isOpen) + const [action, setAction] = useState("") + const inputRef = useRef(null) + + useEffect(() => { + if (isOpen && !prevIsOpen) { + setAction(initialValue.fieldValue) + } + + setPrevIsOpen(isOpen) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]) + + const handleAfterOpen = () => inputRef.current && inputRef.current.focus() + + const handleKeyPress = (e) => { + if (e.keyCode === 13 && e.metaKey) { + // TODO check whether we had an original number, boolean or string type? + let castedValue: string | number | boolean = action + switch (typeof initialValue.fieldValue) { + case "number": + castedValue = castedValue === "" ? null : Number(castedValue) + break + case "boolean": + castedValue = e.target.checked + break + case "string": + default: + break // No conversion needed for strings + } + + const newUpdates = { ...initialValue, fieldValue: castedValue } + onDispatchAction(newUpdates) + setAction("") + onClose() + } + } + + const handleClose = useCallback(() => { + setAction("") + onClose() + }, [onClose]) + + const handleChange = useCallback((e) => { + setAction(e.target.value) + }, []) + + return ( + + {isDarwin ? KEY_MAPS.command : KEY_MAPS.ctrl} + ENTER Update + + } + > + +

Modify the field for the following cache key:

+
+ + + Cache ID: {cacheKey} | Property: {initialValue.fieldName} + + {/* TODO string = input type text, number = input type number. boolean type checkbox */} + {typeof initialValue.fieldValue === "string" && ( + + )} + {typeof initialValue.fieldValue === "number" && ( + + )} + {typeof initialValue.fieldValue === "boolean" && ( + + )} + +
+ ) +} + +export default ApolloUpdateCacheValueModal diff --git a/lib/reactotron-mst/src/reactotron-mst.ts b/lib/reactotron-mst/src/reactotron-mst.ts index 9eed8dc04..599e083fb 100644 --- a/lib/reactotron-mst/src/reactotron-mst.ts +++ b/lib/reactotron-mst/src/reactotron-mst.ts @@ -484,6 +484,7 @@ export function mst(opts: MstPluginOptions = {}) { // --- Reactotron plugin interface --------------------------------- return { + name: "mst", // Fires when we receive a command from the Reactotron app. onCommand, diff --git a/lib/reactotron-react-native/src/plugins/asyncStorage.ts b/lib/reactotron-react-native/src/plugins/asyncStorage.ts index 0a998e205..3870bfd05 100644 --- a/lib/reactotron-react-native/src/plugins/asyncStorage.ts +++ b/lib/reactotron-react-native/src/plugins/asyncStorage.ts @@ -188,6 +188,7 @@ const asyncStorage = (options?: AsyncStorageOptions) => (reactotron: ReactotronC } return { + name: "async-storage", features: { trackAsyncStorage, untrackAsyncStorage, diff --git a/lib/reactotron-react-native/src/plugins/devTools.ts b/lib/reactotron-react-native/src/plugins/devTools.ts index 28708b5e4..043f1e39e 100644 --- a/lib/reactotron-react-native/src/plugins/devTools.ts +++ b/lib/reactotron-react-native/src/plugins/devTools.ts @@ -51,6 +51,7 @@ const getDevMenu = (): Spec => { const devTools = () => () => { return { + name: "dev-tools", onCommand: (command) => { if (command.type !== "devtools.open" && command.type !== "devtools.reload") return diff --git a/lib/reactotron-react-native/src/plugins/networking.ts b/lib/reactotron-react-native/src/plugins/networking.ts index ea1049004..2b1cfc590 100644 --- a/lib/reactotron-react-native/src/plugins/networking.ts +++ b/lib/reactotron-react-native/src/plugins/networking.ts @@ -148,6 +148,7 @@ const networking = } return { + name: "networking", onConnect: () => { // register our monkey-patch XHRInterceptor.setSendCallback(onSend) diff --git a/lib/reactotron-react-native/src/plugins/openInEditor.ts b/lib/reactotron-react-native/src/plugins/openInEditor.ts index 1b2bfec04..892a336a1 100644 --- a/lib/reactotron-react-native/src/plugins/openInEditor.ts +++ b/lib/reactotron-react-native/src/plugins/openInEditor.ts @@ -14,6 +14,7 @@ const openInEditor = const options = Object.assign({}, DEFAULTS, pluginConfig) return { + name: "open-in-editor", onCommand: (command) => { if (command.type !== "editor.open") return const { payload } = command diff --git a/lib/reactotron-react-native/src/plugins/overlay/index.tsx b/lib/reactotron-react-native/src/plugins/overlay/index.tsx index 69b41ff73..5e5561cf1 100644 --- a/lib/reactotron-react-native/src/plugins/overlay/index.tsx +++ b/lib/reactotron-react-native/src/plugins/overlay/index.tsx @@ -10,6 +10,7 @@ export default function OverlayCreator() { const emitter = mitt() return { + name: "overlay", /** * Fires when any Reactotron message arrives. */ @@ -22,13 +23,12 @@ export default function OverlayCreator() { features: { overlay: (WrappedComponent: React.ComponentType) => - (props: Record = {}) => - ( - - - - - ), + (props: Record = {}) => ( + + + + + ), }, } satisfies Plugin } diff --git a/lib/reactotron-react-native/src/plugins/storybook/index.tsx b/lib/reactotron-react-native/src/plugins/storybook/index.tsx index f6ff98685..6fcd24681 100644 --- a/lib/reactotron-react-native/src/plugins/storybook/index.tsx +++ b/lib/reactotron-react-native/src/plugins/storybook/index.tsx @@ -11,6 +11,7 @@ export default () => () => { const emitter = mitt() return { + name: "storybook", onCommand: (command) => { if (command.type !== "storybook") return // relay this payload on to the emitter diff --git a/lib/reactotron-react-native/src/plugins/trackGlobalErrors.ts b/lib/reactotron-react-native/src/plugins/trackGlobalErrors.ts index 6e5be2096..4913d628c 100644 --- a/lib/reactotron-react-native/src/plugins/trackGlobalErrors.ts +++ b/lib/reactotron-react-native/src/plugins/trackGlobalErrors.ts @@ -121,6 +121,7 @@ const trackGlobalErrors = (options?: TrackGlobalErrorsOptions) => (reactotron: R // the reactotron plugin interface return { + name: "track-global-errors", onConnect: () => { LogBox.addException = new Proxy(LogBox.addException, { apply: function (target, thisArg, argumentsList: Parameters) { diff --git a/lib/reactotron-react-native/src/plugins/trackGlobalLogs.ts b/lib/reactotron-react-native/src/plugins/trackGlobalLogs.ts index e6e6c1290..11feb5fbb 100644 --- a/lib/reactotron-react-native/src/plugins/trackGlobalLogs.ts +++ b/lib/reactotron-react-native/src/plugins/trackGlobalLogs.ts @@ -14,6 +14,7 @@ const trackGlobalLogs = () => (reactotron: ReactotronCore) => { const client = reactotron as ReactotronCore & InferFeatures return { + name: "track-global-logs", onConnect: () => { const originalConsoleLog = console.log console.log = (...args: Parameters) => { diff --git a/lib/reactotron-redux/src/index.ts b/lib/reactotron-redux/src/index.ts index 3ae3b822f..b0789c664 100644 --- a/lib/reactotron-redux/src/index.ts +++ b/lib/reactotron-redux/src/index.ts @@ -24,6 +24,7 @@ function reactotronRedux(pluginConfig: PluginConfig = {}) { function plugin(reactotron: Client) { return { + name: "redux", // Fires when we receive a command from the Reactotron app. onCommand: createCommandHander(reactotron, mergedPluginConfig, onReduxStoreCreation), diff --git a/scripts/generate-plugin.mjs b/scripts/generate-plugin.mjs index 9bf93fea1..2d1cbea86 100644 --- a/scripts/generate-plugin.mjs +++ b/scripts/generate-plugin.mjs @@ -53,7 +53,7 @@ fs.writeFileSync( ); fs.writeFileSync( - path.join(targetDir, `rollup.config.ts`), + path.join(targetDir, `rollup.config.cjs`), createTemplateRollupConfig({ pluginName }) ); diff --git a/yarn.lock b/yarn.lock index b20f3ea52..12f2a76b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50,6 +50,43 @@ __metadata: languageName: node linkType: hard +"@apollo/client@npm:^3.8.3, @apollo/client@npm:^3.9.4": + version: 3.9.4 + resolution: "@apollo/client@npm:3.9.4" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.1.1" + "@wry/caches": "npm:^1.0.0" + "@wry/equality": "npm:^0.5.6" + "@wry/trie": "npm:^0.5.0" + graphql-tag: "npm:^2.12.6" + hoist-non-react-statics: "npm:^3.3.2" + optimism: "npm:^0.18.0" + prop-types: "npm:^15.7.2" + rehackt: "npm:0.0.4" + response-iterator: "npm:^0.2.6" + symbol-observable: "npm:^4.0.0" + ts-invariant: "npm:^0.10.3" + tslib: "npm:^2.3.0" + zen-observable-ts: "npm:^1.2.5" + peerDependencies: + graphql: ^15.0.0 || ^16.0.0 + graphql-ws: ^5.5.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + checksum: 10/0f17e9e29a7450c9b7b0498e0959ab0ab6837fe90475f0bd081a080c19e64e81edb663fcdc918e4c39dd013bda14fc798a82db5dd427ea2098ae697abad94d46 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.10.4, @babel/code-frame@npm:~7.10.4": version: 7.10.4 resolution: "@babel/code-frame@npm:7.10.4" @@ -162,6 +199,20 @@ __metadata: languageName: node linkType: hard +"@babel/eslint-parser@npm:^7.23.10": + version: 7.23.10 + resolution: "@babel/eslint-parser@npm:7.23.10" + dependencies: + "@nicolo-ribaudo/eslint-scope-5-internals": "npm:5.1.1-v1" + eslint-visitor-keys: "npm:^2.1.0" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 + checksum: 10/eb62ad6a1098836331317be978ebd5991a9257d58118062f252b002e995b4f35b76a5dc976b07d84d21e64c8395587a044c5e6e444b3b69ab53e50a18facf2af + languageName: node + linkType: hard + "@babel/generator@npm:7.2.0": version: 7.2.0 resolution: "@babel/generator@npm:7.2.0" @@ -2319,7 +2370,18 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.4": +"@babel/types@npm:^7.24.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2": + version: 7.25.2 + resolution: "@babel/types@npm:7.25.2" + dependencies: + "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-validator-identifier": "npm:^7.24.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10/ccf5399db1dcd6dd87b84a6f7bc8dd241e04a326f4f038c973c26ccb69cd360c8f2276603f584c58fd94da95229313060b27baceb0d9b18a435742d3f616afd1 + languageName: node + linkType: hard + +"@babel/types@npm:^7.24.8, @babel/types@npm:^7.25.4": version: 7.25.4 resolution: "@babel/types@npm:7.25.4" dependencies: @@ -2689,6 +2751,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:8.57.0": + version: 8.57.0 + resolution: "@eslint/js@npm:8.57.0" + checksum: 10/3c501ce8a997cf6cbbaf4ed358af5492875e3550c19b9621413b82caa9ae5382c584b0efa79835639e6e0ddaa568caf3499318e5bdab68643ef4199dce5eb0a0 + languageName: node + linkType: hard + "@eslint/js@npm:8.57.1": version: 8.57.1 resolution: "@eslint/js@npm:8.57.1" @@ -3137,7 +3206,7 @@ __metadata: languageName: node linkType: hard -"@graphql-typed-document-node/core@npm:^3.1.0": +"@graphql-typed-document-node/core@npm:^3.1.0, @graphql-typed-document-node/core@npm:^3.1.1": version: 3.2.0 resolution: "@graphql-typed-document-node/core@npm:3.2.0" peerDependencies: @@ -3162,7 +3231,7 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.13": +"@humanwhocodes/config-array@npm:^0.11.13, @humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" dependencies: @@ -6936,6 +7005,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^29.5.12": + version: 29.5.12 + resolution: "@types/jest@npm:29.5.12" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10/312e8dcf92cdd5a5847d6426f0940829bca6fe6b5a917248f3d7f7ef5d85c9ce78ef05e47d2bbabc40d41a930e0e36db2d443d2610a9e3db9062da2d5c904211 + languageName: node + linkType: hard + "@types/jsdom@npm:^20.0.0": version: 20.0.1 resolution: "@types/jsdom@npm:20.0.1" @@ -7191,6 +7270,13 @@ __metadata: languageName: node linkType: hard +"@types/ramda@npm:^0.25.50": + version: 0.25.51 + resolution: "@types/ramda@npm:0.25.51" + checksum: 10/b2f81d6a1b6edde32fc5c1a0db483c02c45b959927f3b5c35acae2bf737bb265b93c9149ff98fa28f9e81c040df4dc73bab27d08db0ae3dcdcff6b325395fb9b + languageName: node + linkType: hard + "@types/ramda@npm:^0.28.0": version: 0.28.25 resolution: "@types/ramda@npm:0.28.25" @@ -8019,6 +8105,51 @@ __metadata: languageName: node linkType: hard +"@wry/caches@npm:^1.0.0": + version: 1.0.1 + resolution: "@wry/caches@npm:1.0.1" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/055f592ee52b5fd9aa86e274e54e4a8b2650f619000bf6f61880ce14aaf47eb2ab34f3ada2eab964fe8b2f19bf8097ecacddcea4638fcc64c3d3a0a512aaa07c + languageName: node + linkType: hard + +"@wry/context@npm:^0.7.0": + version: 0.7.4 + resolution: "@wry/context@npm:0.7.4" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/70d648949a97a035b2be2d6ddb716d4162113e850ab2c4c86331b2da94a7e826204080ce04eee2a95665bd3a0b245bf2ea3aae9adfa57b004ae0d2d49bdb5c8f + languageName: node + linkType: hard + +"@wry/equality@npm:^0.5.6": + version: 0.5.7 + resolution: "@wry/equality@npm:0.5.7" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/69dccf33c0c41fd7ec5550f5703b857c6484a949412ad747001da941270ea436648c3ab988a2091765304249585ac30c7b417fad8be9a7ce19c1221f71548e35 + languageName: node + linkType: hard + +"@wry/trie@npm:^0.4.3": + version: 0.4.3 + resolution: "@wry/trie@npm:0.4.3" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/106e021125cfafd22250a6631a0438a6a3debae7bd73f6db87fe42aa0757fe67693db0dfbe200ae1f60ba608c3e09ddb8a4e2b3527d56ed0a7e02aa0ee4c94e1 + languageName: node + linkType: hard + +"@wry/trie@npm:^0.5.0": + version: 0.5.0 + resolution: "@wry/trie@npm:0.5.0" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/578a08f3a96256c9b163230337183d9511fd775bdfe147a30561ccaacedc9ce33b9731ee6e591bb1f5f53e41b26789e519b47dff5100c7bf4e1cd2df3062f797 + languageName: node + linkType: hard + "@xmldom/xmldom@npm:^0.8.8": version: 0.8.10 resolution: "@xmldom/xmldom@npm:0.8.10" @@ -9144,7 +9275,7 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:^29.2.1, babel-jest@npm:^29.7.0": +"babel-jest@npm:^29.2.1, babel-jest@npm:^29.4.3, babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" dependencies: @@ -14294,6 +14425,54 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.35.0": + version: 8.57.0 + resolution: "eslint@npm:8.57.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.6.1" + "@eslint/eslintrc": "npm:^2.1.4" + "@eslint/js": "npm:8.57.0" + "@humanwhocodes/config-array": "npm:^0.11.14" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@nodelib/fs.walk": "npm:^1.2.8" + "@ungap/structured-clone": "npm:^1.2.0" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.2" + debug: "npm:^4.3.2" + doctrine: "npm:^3.0.0" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^7.2.2" + eslint-visitor-keys: "npm:^3.4.3" + espree: "npm:^9.6.1" + esquery: "npm:^1.4.2" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^6.0.1" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + globals: "npm:^13.19.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + is-path-inside: "npm:^3.0.3" + js-yaml: "npm:^4.1.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + levn: "npm:^0.4.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + strip-ansi: "npm:^6.0.1" + text-table: "npm:^0.2.0" + bin: + eslint: bin/eslint.js + checksum: 10/00496e218b23747a7a9817bf58b522276d0dc1f2e546dceb4eea49f9871574088f72f1f069a6b560ef537efa3a75261b8ef70e51ef19033da1cc4c86a755ef15 + languageName: node + linkType: hard + "eslint@npm:^8.53.0": version: 8.56.0 resolution: "eslint@npm:8.56.0" @@ -14540,6 +14719,7 @@ __metadata: version: 0.0.0-use.local resolution: "example-app@workspace:apps/example-app" dependencies: + "@apollo/client": "npm:^3.9.4" "@babel/core": "npm:^7.24.0" "@babel/plugin-proposal-export-namespace-from": "npm:^7.18.9" "@babel/plugin-proposal-nullish-coalescing-operator": "npm:^7.0.0" @@ -14584,6 +14764,7 @@ __metadata: expo-localization: "npm:~15.0.3" expo-splash-screen: "npm:~0.27.7" expo-status-bar: "npm:~1.12.1" + graphql: "npm:^16.8.1" i18n-js: "npm:3.9.2" jest: "npm:^29.2.1" jest-expo: "npm:~51.0.4" @@ -14603,6 +14784,7 @@ __metadata: react-native-web: "npm:~0.19.6" react-redux: "npm:^9.1.0" react-test-renderer: "npm:18.2.0" + reactotron-apollo-client: "workspace:*" reactotron-core-client: "workspace:*" reactotron-mst: "workspace:*" reactotron-react-js: "workspace:*" @@ -16483,7 +16665,7 @@ __metadata: languageName: node linkType: hard -"graphql-tag@npm:^2.10.1": +"graphql-tag@npm:^2.10.1, graphql-tag@npm:^2.12.6": version: 2.12.6 resolution: "graphql-tag@npm:2.12.6" dependencies: @@ -16501,6 +16683,13 @@ __metadata: languageName: node linkType: hard +"graphql@npm:^16.8.0, graphql@npm:^16.8.1": + version: 16.8.1 + resolution: "graphql@npm:16.8.1" + checksum: 10/7a09d3ec5f75061afe2bd2421a2d53cf37273d2ecaad8f34febea1f1ac205dfec2834aec3419fa0a10fcc9fb345863b2f893562fb07ea825da2ae82f6392893c + languageName: node + linkType: hard + "gud@npm:^1.0.0": version: 1.0.0 resolution: "gud@npm:1.0.0" @@ -16860,7 +17049,7 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.3.0": +"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -22124,6 +22313,18 @@ __metadata: languageName: node linkType: hard +"optimism@npm:^0.18.0": + version: 0.18.0 + resolution: "optimism@npm:0.18.0" + dependencies: + "@wry/caches": "npm:^1.0.0" + "@wry/context": "npm:^0.7.0" + "@wry/trie": "npm:^0.4.3" + tslib: "npm:^2.3.0" + checksum: 10/b461968008eb7aafd5b5dd63b81fd41fbd907f39858bdd5190f10b71db6a5bf54541cdb3d2a569b2bf5585ca917ac192f953e6239d81702a4391fdb476a00ae8 + languageName: node + linkType: hard + "optionator@npm:^0.9.1, optionator@npm:^0.9.3": version: 0.9.3 resolution: "optionator@npm:0.9.3" @@ -23580,6 +23781,13 @@ __metadata: languageName: node linkType: hard +"ramda@npm:^0.25.0": + version: 0.25.0 + resolution: "ramda@npm:0.25.0" + checksum: 10/f16715fe64e74925fe209746bffe945e20f95a01c82d32aadbd255d1053fc0d1f0a4233005e302a80acaa822d89764a793f51e69c10930efbb25cb8d23c97d8f + languageName: node + linkType: hard + "randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -24521,7 +24729,7 @@ __metadata: languageName: node linkType: hard -"react-transition-group@npm:^4.3.0": +"react-transition-group@npm:^4.3.0, react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5" dependencies: @@ -24604,6 +24812,51 @@ __metadata: languageName: unknown linkType: soft +"reactotron-apollo-client@workspace:*, reactotron-apollo-client@workspace:lib/reactotron-apollo-client": + version: 0.0.0-use.local + resolution: "reactotron-apollo-client@workspace:lib/reactotron-apollo-client" + dependencies: + "@apollo/client": "npm:^3.8.3" + "@babel/core": "npm:^7.23.2" + "@babel/eslint-parser": "npm:^7.23.10" + "@babel/preset-typescript": "npm:^7.23.2" + "@types/jest": "npm:^29.5.12" + "@types/node": "npm:^18.18.8" + "@types/ramda": "npm:^0.25.50" + "@typescript-eslint/eslint-plugin": "npm:^6.7.5" + "@typescript-eslint/parser": "npm:^6.7.5" + babel-eslint: "npm:^10.1.0" + babel-jest: "npm:^29.4.3" + eslint: "npm:^8.35.0" + eslint-config-prettier: "npm:^9.0.0" + eslint-config-standard: "npm:^17.1.0" + eslint-plugin-import: "npm:^2.29.0" + eslint-plugin-n: "npm:^16.2.0" + eslint-plugin-promise: "npm:^6.1.1" + eslint-plugin-react: "npm:^7.33.2" + eslint-plugin-react-hooks: "npm:^4.6.0" + eslint-plugin-standard: "npm:^5.0.0" + graphql: "npm:^16.8.0" + jest: "npm:^29.7.0" + prettier: "npm:^3.0.3" + ramda: "npm:^0.25.0" + reactotron-core-client: "workspace:*" + rollup: "npm:^1.1.2" + rollup-plugin-babel: "npm:^4.4.0" + rollup-plugin-babel-minify: "npm:^7.0.0" + rollup-plugin-filesize: "npm:^6.0.1" + rollup-plugin-node-resolve: "npm:^4.0.0" + rollup-plugin-resolve: "npm:^0.0.1-predev.1" + rollup-plugin-terser: "npm:^7.0.2" + testdouble: "npm:^3.20.0" + ts-jest: "npm:^29.1.1" + typescript: "npm:^4.9.5" + peerDependencies: + "@apollo/client": "*" + reactotron-core-client: "*" + languageName: unknown + linkType: soft + "reactotron-app@workspace:apps/reactotron-app": version: 0.0.0-use.local resolution: "reactotron-app@workspace:apps/reactotron-app" @@ -24655,6 +24908,7 @@ __metadata: react-router-dom: "npm:^6.18.0" react-test-renderer: "npm:18.2.0" react-tooltip: "npm:4.5.1" + react-transition-group: "npm:^4.4.5" reactotron-core-contract: "workspace:*" reactotron-core-server: "workspace:*" reactotron-core-ui: "workspace:*" @@ -25437,6 +25691,21 @@ __metadata: languageName: node linkType: hard +"rehackt@npm:0.0.4": + version: 0.0.4 + resolution: "rehackt@npm:0.0.4" + peerDependencies: + "@types/react": "*" + react: "*" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + checksum: 10/f2811c24847f7a03935c3b2093a01439f883550f6022e6d220574d0f4333fc24dd1bd746bb579038723eef1c48a2ce3002dd91aea9c2e15d2bd105ed6786f8c1 + languageName: node + linkType: hard + "relateurl@npm:^0.2.7": version: 0.2.7 resolution: "relateurl@npm:0.2.7" @@ -25713,6 +25982,13 @@ __metadata: languageName: node linkType: hard +"response-iterator@npm:^0.2.6": + version: 0.2.6 + resolution: "response-iterator@npm:0.2.6" + checksum: 10/ef7c74693ef3891461955a666e753585b298fe0de1baaf0d190e7a6818e4311e459d72f4a36f04aa8f49eda9b5f97124e5534be01e40d9e008795125d0bbb374 + languageName: node + linkType: hard + "responselike@npm:^2.0.0": version: 2.0.1 resolution: "responselike@npm:2.0.1" @@ -25993,6 +26269,20 @@ __metadata: languageName: node linkType: hard +"rollup-plugin-terser@npm:^7.0.2": + version: 7.0.2 + resolution: "rollup-plugin-terser@npm:7.0.2" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + jest-worker: "npm:^26.2.1" + serialize-javascript: "npm:^4.0.0" + terser: "npm:^5.0.0" + peerDependencies: + rollup: ^2.0.0 + checksum: 10/af84bb7a7a894cd00852b6486528dfb8653cf94df4c126f95f389a346f401d054b08c46bee519a2ab6a22b33804d1d6ac6d8c90b1b2bf8fffb097eed73fc3c72 + languageName: node + linkType: hard + "rollup-pluginutils@npm:^2.3.0, rollup-pluginutils@npm:^2.6.0, rollup-pluginutils@npm:^2.8.1": version: 2.8.2 resolution: "rollup-pluginutils@npm:2.8.2" @@ -26372,7 +26662,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" dependencies: @@ -26383,6 +26673,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.4": + version: 7.6.0 + resolution: "semver@npm:7.6.0" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 10/1b41018df2d8aca5a1db4729985e8e20428c650daea60fcd16e926e9383217d00f574fab92d79612771884a98d2ee2a1973f49d630829a8d54d6570defe62535 + languageName: node + linkType: hard + "semver@npm:^7.6.0": version: 7.6.3 resolution: "semver@npm:7.6.3" @@ -27908,6 +28209,13 @@ __metadata: languageName: node linkType: hard +"symbol-observable@npm:^4.0.0": + version: 4.0.0 + resolution: "symbol-observable@npm:4.0.0" + checksum: 10/983aef3912ad080fc834b9ad115d44bc2994074c57cea4fb008e9f7ab9bb4118b908c63d9edc861f51257bc0595025510bdf7263bb09d8953a6929f240165c24 + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -28137,6 +28445,20 @@ __metadata: languageName: node linkType: hard +"terser@npm:^5.0.0": + version: 5.36.0 + resolution: "terser@npm:5.36.0" + dependencies: + "@jridgewell/source-map": "npm:^0.3.3" + acorn: "npm:^8.8.2" + commander: "npm:^2.20.0" + source-map-support: "npm:~0.5.20" + bin: + terser: bin/terser + checksum: 10/52e641419f79d7ccdecd136b9a8e0b03f93cfe3b53cce556253aaabc347d3f2af1745419b9e622abc95d592084dc76e57774b8f9e68d29d543f4dd11c044daf4 + languageName: node + linkType: hard + "terser@npm:^5.15.0, terser@npm:^5.6.0": version: 5.27.0 resolution: "terser@npm:5.27.0" @@ -28499,6 +28821,15 @@ __metadata: languageName: node linkType: hard +"ts-invariant@npm:^0.10.3": + version: 0.10.3 + resolution: "ts-invariant@npm:0.10.3" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10/bb07d56fe4aae69d8860e0301dfdee2d375281159054bc24bf1e49e513fb0835bf7f70a11351344d213a79199c5e695f37ebbf5a447188a377ce0cd81d91ddb5 + languageName: node + linkType: hard + "ts-jest@npm:^29.1.1": version: 29.1.1 resolution: "ts-jest@npm:29.1.1" @@ -30449,6 +30780,22 @@ __metadata: languageName: node linkType: hard +"zen-observable-ts@npm:^1.2.5": + version: 1.2.5 + resolution: "zen-observable-ts@npm:1.2.5" + dependencies: + zen-observable: "npm:0.8.15" + checksum: 10/2384cf92a60e39e7b9735a0696f119684fee0f8bcc81d71474c92d656eca1bc3e87b484a04e97546e56bd539f8756bf97cf21a28a933ff7a94b35a8d217848eb + languageName: node + linkType: hard + +"zen-observable@npm:0.8.15": + version: 0.8.15 + resolution: "zen-observable@npm:0.8.15" + checksum: 10/30eac3f4055d33f446b4cd075d3543da347c2c8e68fbc35c3f5a19fb43be67c6ed27ee136bc8f8933efa547be7ce04957809ad00ee7f1b00a964f199ae6fb514 + languageName: node + linkType: hard + "zod-validation-error@npm:^2.1.0": version: 2.1.0 resolution: "zod-validation-error@npm:2.1.0"