├── .gitattributes ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── apps └── example │ ├── .gitignore │ ├── app.json │ ├── assets │ ├── fonts │ │ └── SpaceMono-Regular.ttf │ └── images │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ └── splash.png │ ├── eslint.config.js │ ├── package.json │ ├── src │ ├── app │ │ ├── _layout.tsx │ │ ├── apollo-client │ │ │ └── index.tsx │ │ ├── async-storage │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── react-native-mmkv │ │ │ └── index.tsx │ │ ├── react-query-time │ │ │ └── index.tsx │ │ ├── react-query │ │ │ ├── [movie].tsx │ │ │ ├── _layout.tsx │ │ │ └── index.tsx │ │ ├── tinybase │ │ │ └── index.tsx │ │ └── vanilla-log-viewer │ │ │ └── index.tsx │ ├── constants │ │ └── Colors.ts │ ├── react-query-time │ │ └── use-time.ts │ └── react-query │ │ ├── components │ │ ├── Divider.tsx │ │ ├── ErrorMessage.tsx │ │ ├── ListItem.tsx │ │ └── LoadingIndicator.tsx │ │ ├── data │ │ └── movies.json │ │ ├── hooks │ │ ├── useAppState.ts │ │ ├── useOnlineManager.ts │ │ ├── useRefreshByUser.ts │ │ └── useRefreshOnFocus.ts │ │ ├── lib │ │ └── api.ts │ │ └── types.ts │ └── tsconfig.json ├── bun.lock ├── package.json ├── packages ├── apollo-client │ ├── .eslintrc.js │ ├── README.md │ ├── babel.config.js │ ├── build │ │ ├── index.d.ts │ │ ├── index.d.ts.map │ │ ├── index.js │ │ ├── index.js.map │ │ ├── types.d.ts │ │ ├── types.d.ts.map │ │ ├── types.js │ │ ├── types.js.map │ │ ├── useApolloClientDevTools.d.ts │ │ ├── useApolloClientDevTools.d.ts.map │ │ ├── useApolloClientDevTools.js │ │ ├── useApolloClientDevTools.js.map │ │ ├── utils.d.ts │ │ ├── utils.d.ts.map │ │ ├── utils.js │ │ └── utils.js.map │ ├── eslint.config.js │ ├── expo-module.config.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ ├── useApolloClientDevTools.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── webui │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── index.js │ │ ├── package.json │ │ ├── src │ │ ├── App.tsx │ │ ├── Details.tsx │ │ ├── Header.tsx │ │ ├── List.tsx │ │ ├── types.ts │ │ └── utils.ts │ │ └── tsconfig.json ├── async-storage │ ├── .eslintrc.js │ ├── README.md │ ├── babel.config.js │ ├── build │ │ ├── index.d.ts │ │ ├── index.d.ts.map │ │ ├── index.js │ │ ├── index.js.map │ │ ├── useAsyncStorageDevTools.d.ts │ │ ├── useAsyncStorageDevTools.d.ts.map │ │ ├── useAsyncStorageDevTools.js │ │ └── useAsyncStorageDevTools.js.map │ ├── eslint.config.js │ ├── expo-module.config.json │ ├── methods.d.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── useAsyncStorageDevTools.ts │ ├── tsconfig.json │ └── webui │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── index.js │ │ ├── package.json │ │ ├── src │ │ ├── App.tsx │ │ ├── AsyncStorageTable.tsx │ │ ├── modal │ │ │ ├── useAddEntryDialog.tsx │ │ │ └── useRemoveEntryModal.tsx │ │ ├── usePluginStore.ts │ │ └── useTableData.tsx │ │ └── tsconfig.json ├── create-dev-plugin │ ├── .eslintrc.js │ ├── .npmignore │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── copyFilesWithTransforms.ts │ │ ├── createAppAdapterProject.ts │ │ ├── createWebUiProject.ts │ │ ├── env.ts │ │ ├── index.ts │ │ ├── resolvePackageManager.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── templates │ │ └── app-adapter │ │ │ ├── $.gitignore │ │ │ ├── babel.config.js │ │ │ ├── eslint.config.js │ │ │ ├── expo-module.config.json │ │ │ ├── package.json │ │ │ ├── scripts │ │ │ └── build-webui.js │ │ │ ├── src │ │ │ ├── index.ts │ │ │ └── {%= project.hookName %}.ts │ │ │ └── tsconfig.json │ └── tsconfig.json ├── react-native-mmkv │ ├── .eslintrc.js │ ├── README.md │ ├── babel.config.js │ ├── build │ │ ├── index.d.ts │ │ ├── index.d.ts.map │ │ ├── index.js │ │ ├── index.js.map │ │ ├── useMMKVDevTools.d.ts │ │ ├── useMMKVDevTools.d.ts.map │ │ ├── useMMKVDevTools.js │ │ └── useMMKVDevTools.js.map │ ├── eslint.config.js │ ├── expo-module.config.json │ ├── methods.d.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── useMMKVDevTools.ts │ ├── tsconfig.json │ └── webui │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── index.js │ │ ├── package.json │ │ ├── src │ │ ├── App.tsx │ │ ├── KeyValueStorageTable.tsx │ │ ├── MMKVStorageTable.tsx │ │ ├── modal │ │ │ ├── useAddEntryDialog.tsx │ │ │ └── useRemoveEntryModal.tsx │ │ ├── useConnectedClient.ts │ │ ├── usePluginStore.ts │ │ └── useTableData.tsx │ │ └── tsconfig.json ├── react-navigation │ ├── .eslintrc.js │ ├── README.md │ ├── babel.config.js │ ├── build │ │ ├── ReduxExtensionAdapter.d.ts │ │ ├── ReduxExtensionAdapter.d.ts.map │ │ ├── ReduxExtensionAdapter.js │ │ ├── ReduxExtensionAdapter.js.map │ │ ├── index.d.ts │ │ ├── index.d.ts.map │ │ ├── index.js │ │ ├── index.js.map │ │ ├── useReactNavigationDevTools.d.ts │ │ ├── useReactNavigationDevTools.d.ts.map │ │ ├── useReactNavigationDevTools.js │ │ └── useReactNavigationDevTools.js.map │ ├── eslint.config.js │ ├── expo-module.config.json │ ├── package.json │ ├── src │ │ ├── ReduxExtensionAdapter.ts │ │ ├── index.ts │ │ └── useReactNavigationDevTools.ts │ ├── tsconfig.json │ └── webui │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── index.js │ │ ├── package.json │ │ ├── src │ │ ├── App.tsx │ │ ├── LinkingTester.tsx │ │ ├── Logs.tsx │ │ ├── RouteMap.tsx │ │ ├── Sidebar.tsx │ │ ├── Typography.tsx │ │ ├── theme.tsx │ │ ├── types.ts │ │ └── usePluginStore.ts │ │ └── tsconfig.json ├── react-query │ ├── .eslintrc.js │ ├── README.md │ ├── babel.config.js │ ├── build │ │ ├── index.d.ts │ │ ├── index.d.ts.map │ │ ├── index.js │ │ ├── index.js.map │ │ ├── useReactQueryDevTools.d.ts │ │ ├── useReactQueryDevTools.d.ts.map │ │ ├── useReactQueryDevTools.js │ │ └── useReactQueryDevTools.js.map │ ├── eslint.config.js │ ├── expo-module.config.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── useReactQueryDevTools.ts │ ├── tsconfig.json │ └── webui │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── index.js │ │ ├── package.json │ │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── DataViewer.tsx │ │ │ └── QuerySidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ │ └── index.ts │ │ └── tsconfig.json ├── tinybase │ ├── .eslintrc.js │ ├── README.md │ ├── babel.config.js │ ├── build │ │ ├── index.d.ts │ │ ├── index.d.ts.map │ │ ├── index.js │ │ ├── index.js.map │ │ ├── useTinyBaseDevTools.d.ts │ │ ├── useTinyBaseDevTools.d.ts.map │ │ ├── useTinyBaseDevTools.js │ │ └── useTinyBaseDevTools.js.map │ ├── eslint.config.js │ ├── expo-module.config.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── useTinyBaseDevTools.ts │ ├── tsconfig.json │ └── webui │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── index.js │ │ ├── package.json │ │ ├── src │ │ └── App.tsx │ │ └── tsconfig.json └── vanilla-log-viewer │ ├── README.md │ ├── expo-module.config.json │ ├── index.d.ts │ ├── index.js │ ├── package.json │ └── webui-dist │ ├── index.html │ ├── script-bundle.js │ ├── script.js │ └── style.css ├── scripts ├── build-webui.js └── prepare-packages.js └── tsconfig.node.json /.gitattributes: -------------------------------------------------------------------------------- 1 | packages/*/build/** -diff linguist-generated 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # create-dev-plugin 23 | packages/create-dev-plugin/build 24 | 25 | # debug 26 | npm-debug.* 27 | yarn-debug.* 28 | yarn-error.* 29 | 30 | # macOS 31 | .DS_Store 32 | *.pem 33 | 34 | # local env files 35 | .env*.local 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | 40 | # misc 41 | .idea 42 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "bracketSameLine": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 38 | # The following patterns were generated by expo-cli 39 | 40 | expo-env.d.ts 41 | # @end expo-cli 42 | 43 | ios/ 44 | android/ 45 | -------------------------------------------------------------------------------- /apps/example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": [ 16 | "**/*" 17 | ], 18 | "newArchEnabled": true, 19 | "ios": { 20 | "supportsTablet": true, 21 | "bundleIdentifier": "dev.expo.devplugins.example" 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/images/adaptive-icon.png", 26 | "backgroundColor": "#ffffff" 27 | }, 28 | "package": "dev.expo.devplugins.example" 29 | }, 30 | "web": { 31 | "bundler": "metro", 32 | "output": "static", 33 | "favicon": "./assets/images/favicon.png" 34 | }, 35 | "plugins": [ 36 | "expo-router", 37 | "expo-font", 38 | "expo-web-browser" 39 | ], 40 | "experiments": { 41 | "typedRoutes": true 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/example/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/dev-plugins/456e3212c7c779a9dc52c494f76bf0f2433c30bc/apps/example/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /apps/example/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/dev-plugins/456e3212c7c779a9dc52c494f76bf0f2433c30bc/apps/example/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /apps/example/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/dev-plugins/456e3212c7c779a9dc52c494f76bf0f2433c30bc/apps/example/assets/images/favicon.png -------------------------------------------------------------------------------- /apps/example/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/dev-plugins/456e3212c7c779a9dc52c494f76bf0f2433c30bc/apps/example/assets/images/icon.png -------------------------------------------------------------------------------- /apps/example/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/dev-plugins/456e3212c7c779a9dc52c494f76bf0f2433c30bc/apps/example/assets/images/splash.png -------------------------------------------------------------------------------- /apps/example/eslint.config.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | const { defineConfig } = require('eslint/config'); 3 | const expoConfig = require('eslint-config-expo/flat'); 4 | 5 | module.exports = defineConfig([ 6 | expoConfig, 7 | { 8 | ignores: ['dist/*'], 9 | }, 10 | ]); 11 | -------------------------------------------------------------------------------- /apps/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo run:android", 8 | "ios": "expo run:ios", 9 | "web": "expo start --web", 10 | "lint": "expo lint" 11 | }, 12 | "dependencies": { 13 | "@apollo/client": "^3.11.10", 14 | "@expo/vector-icons": "^14.1.0", 15 | "@react-native-async-storage/async-storage": "2.1.2", 16 | "@react-native-community/netinfo": "11.4.1", 17 | "@react-navigation/native": "^7.0.0", 18 | "@tanstack/react-query": "^5.59.20", 19 | "expo": "53.0.7", 20 | "expo-font": "~13.3.0", 21 | "expo-linking": "~7.1.4", 22 | "expo-router": "~5.0.4", 23 | "expo-splash-screen": "~0.30.8", 24 | "expo-status-bar": "~2.2.3", 25 | "expo-system-ui": "~5.0.7", 26 | "expo-web-browser": "~14.1.6", 27 | "graphql": "^16.9.0", 28 | "react": "19.0.0", 29 | "react-dom": "19.0.0", 30 | "react-native": "0.79.2", 31 | "react-native-gesture-handler": "~2.24.0", 32 | "react-native-mmkv": "^3.1.0", 33 | "react-native-paper": "^5.12.5", 34 | "react-native-safe-area-context": "5.4.0", 35 | "react-native-screens": "~4.10.0", 36 | "react-native-web": "0.20.0", 37 | "tinybase": "^5.3.8" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.26.0", 41 | "@babel/plugin-transform-private-methods": "^7.25.9", 42 | "@types/react": "~19.0.10", 43 | "eslint": "^9.25.0", 44 | "eslint-config-expo": "~9.2.0", 45 | "typescript": "~5.8.3" 46 | }, 47 | "overrides": { 48 | "react-refresh": "~0.14.0" 49 | }, 50 | "resolutions": { 51 | "react-refresh": "~0.14.0" 52 | }, 53 | "private": true 54 | } 55 | -------------------------------------------------------------------------------- /apps/example/src/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { useReactNavigationDevTools } from '@dev-plugins/react-navigation'; 2 | import FontAwesome from '@expo/vector-icons/FontAwesome'; 3 | import { useFonts } from 'expo-font'; 4 | import { SplashScreen, Stack , useNavigationContainerRef } from 'expo-router'; 5 | import { useEffect } from 'react'; 6 | import { StatusBar } from 'expo-status-bar'; 7 | 8 | export { 9 | // Catch any errors thrown by the Layout component. 10 | ErrorBoundary, 11 | } from 'expo-router'; 12 | 13 | // Prevent the splash screen from auto-hiding before asset loading is complete. 14 | SplashScreen.preventAutoHideAsync(); 15 | 16 | export const unstable_settings = { 17 | initialRouteName: 'index', 18 | }; 19 | 20 | export default function RootLayout() { 21 | const [loaded, error] = useFonts({ 22 | SpaceMono: require('../../assets/fonts/SpaceMono-Regular.ttf'), 23 | ...FontAwesome.font, 24 | }); 25 | 26 | // Expo Router uses Error Boundaries to catch errors in the navigation tree. 27 | useEffect(() => { 28 | if (error) throw error; 29 | }, [error]); 30 | 31 | useEffect(() => { 32 | if (loaded) { 33 | SplashScreen.hideAsync(); 34 | } 35 | }, [loaded]); 36 | 37 | if (!loaded) { 38 | return null; 39 | } 40 | 41 | return ; 42 | } 43 | 44 | function RootLayoutNav() { 45 | const navigationRef = useNavigationContainerRef(); 46 | useReactNavigationDevTools(navigationRef); 47 | 48 | return ( 49 | <> 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /apps/example/src/app/apollo-client/index.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider, ApolloClient, InMemoryCache, useQuery, gql } from '@apollo/client'; 2 | import { StyleSheet, Text, View, Image, ScrollView } from 'react-native'; 3 | import { useApolloClientDevTools } from '@dev-plugins/apollo-client'; 4 | 5 | const client = new ApolloClient({ 6 | uri: 'https://flyby-router-demo.herokuapp.com/', 7 | cache: new InMemoryCache(), 8 | }); 9 | 10 | interface Location { 11 | id: string; 12 | name: string; 13 | description: string; 14 | photo: string; 15 | } 16 | const GET_LOCATIONS = gql` 17 | query GetLocations { 18 | locations { 19 | id 20 | name 21 | description 22 | photo 23 | } 24 | } 25 | `; 26 | 27 | export function Main() { 28 | useApolloClientDevTools(client); 29 | const { loading, error, data } = useQuery<{ locations: Location[] }>(GET_LOCATIONS); 30 | 31 | if (loading) { 32 | return Loading...; 33 | } 34 | if (error) { 35 | return Error: ; 36 | } 37 | const contents = data?.locations.map(({ id, name, description, photo }) => ( 38 | 39 | {name} 40 | 41 | About this location: 42 | {description} 43 | 44 | )); 45 | 46 | return {contents}; 47 | } 48 | 49 | export default function ApolloDemo() { 50 | return ( 51 | 52 |
53 | 54 | ); 55 | } 56 | 57 | const styles = StyleSheet.create({ 58 | container: { 59 | flex: 1, 60 | backgroundColor: '#fff', 61 | justifyContent: 'center', 62 | marginTop: 60, 63 | }, 64 | item: { 65 | padding: 8, 66 | marginVertical: 16, 67 | flexDirection: 'column', 68 | }, 69 | name: { 70 | fontSize: 24, 71 | fontWeight: 'bold', 72 | }, 73 | photo: { 74 | width: 350, 75 | height: 200, 76 | alignSelf: 'center', 77 | marginVertical: 8, 78 | }, 79 | aboutCaption: { 80 | fontSize: 18, 81 | marginVertical: 8, 82 | }, 83 | description: { 84 | fontSize: 12, 85 | }, 86 | }); 87 | -------------------------------------------------------------------------------- /apps/example/src/app/async-storage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAsyncStorageDevTools } from '@dev-plugins/async-storage'; 2 | import AsyncStorage, { useAsyncStorage } from '@react-native-async-storage/async-storage'; 3 | import { KeyValuePair } from '@react-native-async-storage/async-storage/lib/typescript/types'; 4 | import { useCallback, useEffect, useState } from 'react'; 5 | import { Text, TextInput, View } from 'react-native'; 6 | import { IconButton } from 'react-native-paper'; 7 | 8 | function Main() { 9 | useAsyncStorageDevTools(); 10 | 11 | const [key, setKey] = useState(''); 12 | const [value, setValue] = useState(''); 13 | const { setItem, getItem } = useAsyncStorage(key); 14 | 15 | const [allData, setAllData] = useState([]); 16 | 17 | const updateAllData = useCallback(() => { 18 | AsyncStorage.getAllKeys().then((keys) => { 19 | AsyncStorage.multiGet(keys).then((data) => { 20 | setAllData(data); 21 | }); 22 | }); 23 | }, []); 24 | 25 | useEffect(() => { 26 | const interval = setInterval(updateAllData, 1000); 27 | return () => clearInterval(interval); 28 | }, [updateAllData]); 29 | 30 | return ( 31 | 32 | Async Storage 33 | 41 | Key: 42 | { 45 | setKey(text); 46 | setValue(''); 47 | }} 48 | value={key} 49 | /> 50 | 51 | 59 | Value: 60 | 65 | setItem(value).then(() => updateAllData())} 70 | /> 71 | getItem().then((value) => setValue(value ?? ''))} 76 | /> 77 | 78 | 79 | Entries: 80 | {allData.map(([key, value]) => ( 81 | 90 | {key}: 91 | {value} 92 | AsyncStorage.removeItem(key).then(() => updateAllData())} 97 | /> 98 | 99 | ))} 100 | 101 | 102 | ); 103 | } 104 | 105 | export default function AsyncStorageDemo() { 106 | return
; 107 | } 108 | -------------------------------------------------------------------------------- /apps/example/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import FontAwesome from '@expo/vector-icons/FontAwesome'; 2 | import { Link, Stack, type LinkProps } from 'expo-router'; 3 | import { Pressable, StyleSheet, Text, View } from 'react-native'; 4 | import { SafeAreaView } from 'react-native-safe-area-context'; 5 | 6 | export default function TesterScreen() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | function TestCase({ title, route }: { title: string; route: LinkProps['href'] }) { 24 | return ( 25 | 26 | 27 | {title} 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | outerContainer: { 36 | flex: 1, 37 | justifyContent: 'center', 38 | backgroundColor: '#eee', 39 | }, 40 | container: { 41 | borderRadius: 8, 42 | borderWidth: 1, 43 | borderColor: '#ccc', 44 | marginHorizontal: 16, 45 | flexDirection: 'column', 46 | backgroundColor: '#fff', 47 | }, 48 | testCaseContainer: { 49 | flexDirection: 'row', 50 | alignItems: 'center', 51 | justifyContent: 'space-between', 52 | paddingVertical: 16, 53 | paddingHorizontal: 20, 54 | borderBottomWidth: 1, 55 | borderBottomColor: '#ccc', 56 | }, 57 | testCaseText: { 58 | fontSize: 20, 59 | fontWeight: 'bold', 60 | color: '#333', 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /apps/example/src/app/react-native-mmkv/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMMKVDevTools } from '@dev-plugins/react-native-mmkv'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { Text, TextInput, View } from 'react-native'; 4 | import { MMKV, useMMKVString } from 'react-native-mmkv'; 5 | import { IconButton } from 'react-native-paper'; 6 | 7 | const storage = new MMKV(); 8 | 9 | function Main() { 10 | useMMKVDevTools(); 11 | 12 | const [key, setKey] = useState(''); 13 | const [value, setValue] = useState(''); 14 | const [item, setItem] = useMMKVString(key); 15 | 16 | const [allData, setAllData] = useState<[string, string | undefined][]>([]); 17 | 18 | const updateAllData = useCallback(() => { 19 | const keys = storage.getAllKeys(); 20 | const keyValues = keys.map((key) => [key, storage.getString(key)]) as [ 21 | string, 22 | string | undefined, 23 | ][]; 24 | setAllData(keyValues); 25 | }, []); 26 | 27 | useEffect(() => { 28 | const interval = setInterval(updateAllData, 1000); 29 | return () => clearInterval(interval); 30 | }, [updateAllData]); 31 | 32 | return ( 33 | 34 | MMKV 35 | 43 | Key: 44 | { 47 | setKey(text); 48 | setValue(''); 49 | }} 50 | value={key} 51 | /> 52 | 53 | 61 | Value: 62 | 67 | { 72 | setItem(value); 73 | updateAllData(); 74 | }} 75 | /> 76 | { 81 | setValue(item ?? ''); 82 | }} 83 | /> 84 | 85 | 86 | Entries: 87 | {allData.map(([key, value]) => ( 88 | 97 | {key}: 98 | {value} 99 | { 104 | storage.delete(key); 105 | updateAllData(); 106 | }} 107 | /> 108 | 109 | ))} 110 | 111 | 112 | ); 113 | } 114 | 115 | export default function ReactNativeMMKVDemo() { 116 | return
; 117 | } 118 | -------------------------------------------------------------------------------- /apps/example/src/app/react-query/[movie].tsx: -------------------------------------------------------------------------------- 1 | import { ErrorMessage } from '@/react-query/components/ErrorMessage'; 2 | import { LoadingIndicator } from '@/react-query/components/LoadingIndicator'; 3 | import { useRefreshByUser } from '@/react-query/hooks/useRefreshByUser'; 4 | import { fetchMovie, MovieDetails } from '@/react-query/lib/api'; 5 | import { useQuery } from '@tanstack/react-query'; 6 | import { Stack, useLocalSearchParams } from 'expo-router'; 7 | import * as React from 'react'; 8 | import { RefreshControl, ScrollView, StyleSheet, View } from 'react-native'; 9 | import { Paragraph, Title } from 'react-native-paper'; 10 | 11 | export default function MovieDetailsScreen() { 12 | const { movie } = useLocalSearchParams<{ movie: string }>(); 13 | 14 | return ( 15 | <> 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | function MovieDetailsInfo({ movie }: { movie: string }) { 23 | const { isPending, error, data, refetch } = useQuery({ 24 | queryKey: ['movie', movie], 25 | queryFn: () => fetchMovie(movie), 26 | initialData: { title: movie } as MovieDetails, 27 | }); 28 | 29 | const { isRefetchingByUser, refetchByUser } = useRefreshByUser(refetch); 30 | 31 | if (isPending) return ; 32 | if (error) return ; 33 | if (!data) return null; 34 | 35 | return ( 36 | }> 38 | 39 | 40 | {data.title} ({data.year}) 41 | 42 | 43 | {data.info ? ( 44 | <> 45 | 46 | {data.info.plot} 47 | 48 | 49 | 50 | {data.info.actors.slice(0, -1).join(', ') + ' or ' + data.info.actors.slice(-1)} 51 | 52 | 53 | 54 | ) : ( 55 | 56 | )} 57 | 58 | ); 59 | } 60 | 61 | const styles = StyleSheet.create({ 62 | titleRow: { 63 | flexDirection: 'row', 64 | margin: 20, 65 | }, 66 | infoRow: { 67 | flexDirection: 'row', 68 | margin: 20, 69 | }, 70 | actorsRow: { 71 | flexDirection: 'column', 72 | margin: 20, 73 | marginTop: 10, 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /apps/example/src/app/react-query/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from 'expo-router'; 2 | import { AppStateStatus, Platform } from 'react-native'; 3 | import { useReactQueryDevTools } from '@dev-plugins/react-query'; 4 | import { QueryClient, QueryClientProvider, focusManager } from '@tanstack/react-query'; 5 | 6 | import { useAppState } from '@/react-query/hooks/useAppState'; 7 | import { useOnlineManager } from '@/react-query/hooks/useOnlineManager'; 8 | 9 | function onAppStateChange(status: AppStateStatus) { 10 | // React Query already supports in web browser refetch on window focus by default 11 | if (Platform.OS !== 'web') { 12 | focusManager.setFocused(status === 'active'); 13 | } 14 | } 15 | 16 | const queryClient = new QueryClient({ 17 | defaultOptions: { queries: { retry: 2 } }, 18 | }); 19 | 20 | export const unstable_settings = { 21 | initialRouteName: 'index', 22 | }; 23 | 24 | export default function Layout() { 25 | useAppState(onAppStateChange); 26 | useOnlineManager(); 27 | useReactQueryDevTools(queryClient); 28 | 29 | return ( 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/example/src/app/react-query/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { FlatList, RefreshControl, StyleSheet, View } from 'react-native'; 3 | import { Stack, useNavigation } from 'expo-router'; 4 | import { useQuery } from '@tanstack/react-query'; 5 | 6 | import { Divider } from '@/react-query/components/Divider'; 7 | import { ListItem } from '@/react-query/components/ListItem'; 8 | import { useRefreshByUser } from '@/react-query/hooks/useRefreshByUser'; 9 | import { useRefreshOnFocus } from '@/react-query/hooks/useRefreshOnFocus'; 10 | import { fetchMovies, Movie } from '@/react-query/lib/api'; 11 | 12 | export default function MoviesListScreen() { 13 | const navigation = useNavigation(); 14 | const { data, refetch } = useQuery({ 15 | queryKey: ['movies'], 16 | queryFn: fetchMovies, 17 | }); 18 | const { isRefetchingByUser, refetchByUser } = useRefreshByUser(refetch); 19 | useRefreshOnFocus(refetch); 20 | 21 | const onListItemPress = React.useCallback( 22 | (movie: any) => { 23 | console.log('movie:', movie); 24 | // @ts-expect-error: untyped 25 | navigation.navigate('[movie]', { 26 | movie: movie.title, 27 | }); 28 | }, 29 | [navigation] 30 | ); 31 | 32 | const renderItem = React.useCallback( 33 | ({ item }: { item: Movie }) => { 34 | return ; 35 | }, 36 | [onListItemPress] 37 | ); 38 | 39 | return ( 40 | 41 | 42 | item.title} 46 | ItemSeparatorComponent={() => } 47 | refreshControl={ 48 | 49 | } 50 | /> 51 | 52 | ); 53 | } 54 | 55 | const styles = StyleSheet.create({ 56 | container: { 57 | flex: 1, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /apps/example/src/app/tinybase/index.tsx: -------------------------------------------------------------------------------- 1 | import { createStore } from 'tinybase'; 2 | import { Button, Text, View } from 'react-native'; 3 | import { useValue, Provider } from 'tinybase/ui-react'; 4 | import { useTinyBaseDevTools } from '@dev-plugins/tinybase'; 5 | 6 | const store = createStore().setValue('counter', 0); 7 | 8 | /** Silly: sync the full store every time something changes **/ 9 | 10 | function Main() { 11 | useTinyBaseDevTools(store); 12 | const count = useValue('counter'); 13 | 14 | return ( 15 | 16 | Counter: {count} 17 | 19 | 22 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/apollo-client/webui/src/List.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Tabs } from 'antd'; 2 | import React, { Dispatch, memo, SetStateAction } from 'react'; 3 | import { ScrollView } from 'react-native'; 4 | 5 | import { BlockType, Data } from './types'; 6 | 7 | export const TabsEnum = { 8 | query: { key: 'query', value: 'Query', plural: 'Queries' }, 9 | mutation: { key: 'mutation', value: 'Mutation', plural: 'Mutations' }, 10 | cache: { key: 'cache', value: 'Cache', plural: 'Caches' }, 11 | }; 12 | 13 | const TabItem = memo( 14 | ({ 15 | active, 16 | onPress, 17 | data, 18 | }: { 19 | active: boolean; 20 | onPress: Dispatch>; 21 | data: any; 22 | }) => { 23 | return ( 24 | 31 | ); 32 | } 33 | ); 34 | 35 | const { TabPane } = Tabs; 36 | 37 | export function List({ 38 | data, 39 | activeTab, 40 | selectedItem, 41 | onItemSelect, 42 | onTabChange, 43 | }: { 44 | data: Data; 45 | activeTab: string; 46 | selectedItem: BlockType; 47 | onItemSelect: (block: BlockType) => void; 48 | onTabChange: (nextTab: string) => void; 49 | }) { 50 | return ( 51 | 52 | 53 | {/* CACHE */} 54 | 55 | {data?.cache?.map((d, i) => { 56 | const active = activeTab === TabsEnum.cache.key && selectedItem?.name === d?.name; 57 | 58 | return ( 59 | 60 | ); 61 | })} 62 | 63 | {/* QUERY */} 64 | 65 | {data?.queries?.map((d) => { 66 | const active = activeTab === TabsEnum.query.key && selectedItem?.id === d?.id; 67 | 68 | return ( 69 | 70 | ); 71 | })} 72 | 73 | {/* MUTATION */} 74 | 75 | {data?.mutations?.map((d) => { 76 | const active = activeTab === TabsEnum.mutation.key && selectedItem?.id === d?.id; 77 | 78 | return ( 79 | 80 | ); 81 | })} 82 | 83 | 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /packages/apollo-client/webui/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ArrayOfMutations, ArrayOfQuery } from '../../src/types'; 2 | 3 | export type TabItemType = { id: string; name: string | null }; 4 | 5 | export type RawMutationBody = { 6 | id: string; 7 | name: string | null; 8 | body: string; 9 | variables: object; 10 | }; 11 | 12 | export type RawQueryBody = { 13 | id: string; 14 | name: string | null; 15 | cachedData: object; 16 | }; 17 | 18 | export type RawData = { 19 | id: string; 20 | lastUpdateAt: Date; 21 | queries: ArrayOfQuery; 22 | mutations: ArrayOfMutations; 23 | cache: BlockType[]; 24 | }; 25 | 26 | export type Data = { 27 | id: string; 28 | lastUpdateAt: Date; 29 | queries: BlockType[]; 30 | mutations: BlockType[]; 31 | cache: BlockType[]; 32 | }; 33 | 34 | export type Events = { 35 | 'GQL:response': RawData; 36 | 'GQL:request': Data; 37 | 'GQL:ack': boolean; 38 | }; 39 | 40 | export type BlockType = { 41 | id?: string; 42 | operationType?: string; 43 | name?: string | null; 44 | blocks?: { 45 | blockType: string; 46 | blockLabel: string; 47 | blockValue: any; 48 | }[]; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/apollo-client/webui/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { BlockType } from './types'; 2 | import { ArrayOfMutations, ArrayOfQuery } from '../../src/types'; 3 | 4 | function createQueryBlocks(queries: ArrayOfQuery) { 5 | return queries.map((query) => { 6 | return { 7 | id: query?.id, 8 | name: query?.name, 9 | operationType: 'Query', 10 | blocks: [ 11 | // { 12 | // blockType: "GQLString", 13 | // blockLabel: "Query String", 14 | // blockValue: query.queryString, 15 | // }, 16 | { 17 | blockType: 'Object', 18 | blockLabel: 'Query Variables', 19 | blockValue: query?.variables, 20 | }, 21 | { 22 | blockType: 'Object', 23 | blockLabel: 'Cached Query Data', 24 | blockValue: query?.cachedData, 25 | }, 26 | ], 27 | }; 28 | }); 29 | } 30 | 31 | function createCacheBlock(cacheObject: Record) { 32 | return [...Object.keys(cacheObject || {})].map((c) => { 33 | const cache = cacheObject[c]; 34 | 35 | return { 36 | id: cache?.id || cache.__typename, 37 | name: c, 38 | operationType: 'Cache', 39 | blocks: [ 40 | { 41 | blockType: 'Object', 42 | blockLabel: 'Cached Data', 43 | blockValue: cache, 44 | }, 45 | ], 46 | }; 47 | }); 48 | } 49 | 50 | function createMutationBlocks(mutations: ArrayOfMutations): BlockType[] { 51 | return mutations.map((mutation) => { 52 | // TODO: cached response (options not applicable in apollo 3.5+) 53 | return { 54 | id: mutation?.id, 55 | name: mutation.name, 56 | operationType: 'Mutation', 57 | blocks: [ 58 | { 59 | blockType: 'GQLString', 60 | blockLabel: 'Mutation Query String', 61 | blockValue: mutation.body, 62 | }, 63 | { 64 | blockType: 'Object', 65 | blockLabel: 'Query Variables', 66 | blockValue: mutation.variables, 67 | }, 68 | ], 69 | }; 70 | }); 71 | } 72 | 73 | export { createCacheBlock, createMutationBlocks, createQueryBlocks }; 74 | -------------------------------------------------------------------------------- /packages/apollo-client/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/async-storage/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /packages/async-storage/README.md: -------------------------------------------------------------------------------- 1 | # @dev-plugins/async-storage 2 | 3 | A React Native Async Storage DevTool that can run in an Expo App 4 | 5 | # Installation 6 | 7 | ### Add the package to your project 8 | 9 | ``` 10 | npx expo install @dev-plugins/async-storage 11 | ``` 12 | 13 | ### Integrate async-storage with the DevTool hook 14 | 15 | ```jsx 16 | import { useAsyncStorageDevTools } from '@dev-plugins/async-storage'; 17 | 18 | export default function App() { 19 | useAsyncStorageDevTools(); 20 | 21 | /* ... */ 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/async-storage/babel.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/babel.config.base'); 3 | -------------------------------------------------------------------------------- /packages/async-storage/build/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare let useAsyncStorageDevTools: typeof import('./useAsyncStorageDevTools').useAsyncStorageDevTools; 2 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /packages/async-storage/build/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,IAAI,uBAAuB,EAAE,cAAc,2BAA2B,EAAE,uBAAuB,CAAC"} -------------------------------------------------------------------------------- /packages/async-storage/build/index.js: -------------------------------------------------------------------------------- 1 | export let useAsyncStorageDevTools; 2 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 3 | if (process.env.NODE_ENV !== 'production') { 4 | useAsyncStorageDevTools = require('./useAsyncStorageDevTools').useAsyncStorageDevTools; 5 | } 6 | else { 7 | useAsyncStorageDevTools = () => { }; 8 | } 9 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /packages/async-storage/build/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,IAAI,uBAA2F,CAAC;AAEvG,wEAAwE;AACxE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,uBAAuB,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC,uBAAuB,CAAC;AACzF,CAAC;KAAM,CAAC;IACN,uBAAuB,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AACrC,CAAC","sourcesContent":["export let useAsyncStorageDevTools: typeof import('./useAsyncStorageDevTools').useAsyncStorageDevTools;\n\n// @ts-ignore process.env.NODE_ENV is defined by metro transform plugins\nif (process.env.NODE_ENV !== 'production') {\n useAsyncStorageDevTools = require('./useAsyncStorageDevTools').useAsyncStorageDevTools;\n} else {\n useAsyncStorageDevTools = () => {};\n}\n"]} -------------------------------------------------------------------------------- /packages/async-storage/build/useAsyncStorageDevTools.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This hook registers a devtools plugin for AsyncStorage. 3 | * 4 | * The plugin provides you with the ability to view, add, edit, and remove AsyncStorage entries. 5 | * 6 | * @param props 7 | * @param props.errorHandler - A function that will be called with any errors that occur while communicating 8 | * with the devtools, if not provided errors will be ignored. Setting this is highly recommended. 9 | */ 10 | export declare function useAsyncStorageDevTools({ errorHandler, }?: { 11 | errorHandler?: (error: Error) => void; 12 | }): void; 13 | //# sourceMappingURL=useAsyncStorageDevTools.d.ts.map -------------------------------------------------------------------------------- /packages/async-storage/build/useAsyncStorageDevTools.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"useAsyncStorageDevTools.d.ts","sourceRoot":"","sources":["../src/useAsyncStorageDevTools.ts"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,YAAY,GACb,GAAE;IACD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,QA+EL"} -------------------------------------------------------------------------------- /packages/async-storage/build/useAsyncStorageDevTools.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { useDevToolsPluginClient } from 'expo/devtools'; 3 | import { useCallback, useEffect } from 'react'; 4 | /** 5 | * This hook registers a devtools plugin for AsyncStorage. 6 | * 7 | * The plugin provides you with the ability to view, add, edit, and remove AsyncStorage entries. 8 | * 9 | * @param props 10 | * @param props.errorHandler - A function that will be called with any errors that occur while communicating 11 | * with the devtools, if not provided errors will be ignored. Setting this is highly recommended. 12 | */ 13 | export function useAsyncStorageDevTools({ errorHandler, } = {}) { 14 | const client = useDevToolsPluginClient('async-storage'); 15 | const handleError = useCallback((error) => { 16 | if (error instanceof Error) { 17 | errorHandler?.(error); 18 | } 19 | else { 20 | errorHandler?.(new Error(`Unknown error: ${String(error)}`)); 21 | } 22 | }, [errorHandler]); 23 | useEffect(() => { 24 | const on = (event, listener) => client?.addMessageListener(event, async (params) => { 25 | try { 26 | const result = await listener(params); 27 | client?.sendMessage(`ack:${event}`, { result }); 28 | } 29 | catch (error) { 30 | try { 31 | client?.sendMessage('error', { error }); 32 | handleError(error); 33 | } 34 | catch (e) { 35 | handleError(e); 36 | } 37 | } 38 | }); 39 | const subscriptions = []; 40 | try { 41 | subscriptions.push(on('getAll', async () => { 42 | const keys = await AsyncStorage.getAllKeys(); 43 | return await AsyncStorage.multiGet(keys); 44 | })); 45 | } 46 | catch (e) { 47 | handleError(e); 48 | } 49 | try { 50 | subscriptions.push(on('set', ({ key, value }) => { 51 | if (key !== undefined && value !== undefined) 52 | return AsyncStorage.setItem(key, value); 53 | else 54 | return Promise.resolve(); 55 | })); 56 | } 57 | catch (e) { 58 | handleError(e); 59 | } 60 | try { 61 | subscriptions.push(on('remove', ({ key }) => { 62 | if (key !== undefined) 63 | return AsyncStorage.removeItem(key); 64 | else 65 | return Promise.resolve(); 66 | })); 67 | } 68 | catch (e) { 69 | handleError(e); 70 | } 71 | return () => { 72 | for (const subscription of subscriptions) { 73 | try { 74 | subscription?.remove(); 75 | } 76 | catch (e) { 77 | handleError(e); 78 | } 79 | } 80 | }; 81 | }, [client]); 82 | } 83 | //# sourceMappingURL=useAsyncStorageDevTools.js.map -------------------------------------------------------------------------------- /packages/async-storage/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | const { defineConfig } = require('eslint/config'); 3 | const baseConfig = require('expo-module-scripts/eslint.config.base'); 4 | module.exports = defineConfig([baseConfig]); 5 | -------------------------------------------------------------------------------- /packages/async-storage/expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["devtools"], 3 | "devtools": { 4 | "webpageRoot": "dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/async-storage/methods.d.ts: -------------------------------------------------------------------------------- 1 | export type Method = 'getAll' | 'set' | 'remove'; 2 | export type MethodAck = `ack:${Method}`; 3 | -------------------------------------------------------------------------------- /packages/async-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/async-storage", 3 | "version": "0.3.1", 4 | "description": "Expo DevTools Plugin for React Navigation", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "expo-module build", 10 | "clean": "expo-module clean", 11 | "lint": "expo-module lint", 12 | "test": "expo-module test", 13 | "prepare": "expo-module prepare && node ../../scripts/build-webui.js async-storage", 14 | "prepublishOnly": "expo-module prepublishOnly", 15 | "expo-module": "expo-module" 16 | }, 17 | "homepage": "https://docs.expo.dev/versions/latest/sdk/image/", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/expo/dev-plugins.git", 21 | "directory": "packages/async-storage" 22 | }, 23 | "keywords": [ 24 | "expo", 25 | "devtools" 26 | ], 27 | "files": [ 28 | "build", 29 | "dist", 30 | "expo-module.config.json", 31 | "src" 32 | ], 33 | "author": "jthoward64", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@react-native-async-storage/async-storage": "^1.24.0", 37 | "expo": "53.0.7", 38 | "expo-module-scripts": "^4.1.7", 39 | "typescript": "~5.8.3" 40 | }, 41 | "peerDependencies": { 42 | "@react-native-async-storage/async-storage": "^1.0.0", 43 | "expo": "^53.0.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/async-storage/src/index.ts: -------------------------------------------------------------------------------- 1 | export let useAsyncStorageDevTools: typeof import('./useAsyncStorageDevTools').useAsyncStorageDevTools; 2 | 3 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 4 | if (process.env.NODE_ENV !== 'production') { 5 | useAsyncStorageDevTools = require('./useAsyncStorageDevTools').useAsyncStorageDevTools; 6 | } else { 7 | useAsyncStorageDevTools = () => {}; 8 | } 9 | -------------------------------------------------------------------------------- /packages/async-storage/src/useAsyncStorageDevTools.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 3 | import { useCallback, useEffect } from 'react'; 4 | 5 | import { Method } from '../methods'; 6 | 7 | /** 8 | * This hook registers a devtools plugin for AsyncStorage. 9 | * 10 | * The plugin provides you with the ability to view, add, edit, and remove AsyncStorage entries. 11 | * 12 | * @param props 13 | * @param props.errorHandler - A function that will be called with any errors that occur while communicating 14 | * with the devtools, if not provided errors will be ignored. Setting this is highly recommended. 15 | */ 16 | export function useAsyncStorageDevTools({ 17 | errorHandler, 18 | }: { 19 | errorHandler?: (error: Error) => void; 20 | } = {}) { 21 | const client = useDevToolsPluginClient('async-storage'); 22 | 23 | const handleError = useCallback( 24 | (error: unknown) => { 25 | if (error instanceof Error) { 26 | errorHandler?.(error); 27 | } else { 28 | errorHandler?.(new Error(`Unknown error: ${String(error)}`)); 29 | } 30 | }, 31 | [errorHandler] 32 | ); 33 | 34 | useEffect(() => { 35 | const on = ( 36 | event: Method, 37 | listener: (params: { key?: string; value?: string }) => Promise 38 | ) => 39 | client?.addMessageListener(event, async (params: { key?: string; value?: string }) => { 40 | try { 41 | const result = await listener(params); 42 | 43 | client?.sendMessage(`ack:${event}`, { result }); 44 | } catch (error) { 45 | try { 46 | client?.sendMessage('error', { error }); 47 | handleError(error); 48 | } catch (e) { 49 | handleError(e); 50 | } 51 | } 52 | }); 53 | 54 | const subscriptions: (EventSubscription | undefined)[] = []; 55 | 56 | try { 57 | subscriptions.push( 58 | on('getAll', async () => { 59 | const keys = await AsyncStorage.getAllKeys(); 60 | return await AsyncStorage.multiGet(keys); 61 | }) 62 | ); 63 | } catch (e) { 64 | handleError(e); 65 | } 66 | 67 | try { 68 | subscriptions.push( 69 | on('set', ({ key, value }) => { 70 | if (key !== undefined && value !== undefined) return AsyncStorage.setItem(key, value); 71 | else return Promise.resolve(); 72 | }) 73 | ); 74 | } catch (e) { 75 | handleError(e); 76 | } 77 | 78 | try { 79 | subscriptions.push( 80 | on('remove', ({ key }) => { 81 | if (key !== undefined) return AsyncStorage.removeItem(key); 82 | else return Promise.resolve(); 83 | }) 84 | ); 85 | } catch (e) { 86 | handleError(e); 87 | } 88 | 89 | return () => { 90 | for (const subscription of subscriptions) { 91 | try { 92 | subscription?.remove(); 93 | } catch (e) { 94 | handleError(e); 95 | } 96 | } 97 | }; 98 | }, [client]); 99 | } 100 | -------------------------------------------------------------------------------- /packages/async-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/async-storage/webui/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "web": { 4 | "bundler": "metro" 5 | }, 6 | "experiments": { 7 | "baseUrl": "/_expo/plugins/@dev-plugins/async-storage" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/async-storage/webui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/async-storage/webui/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /packages/async-storage/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/async-storage-webui", 3 | "description": "The frontend webui for @dev-plugins/async-storage", 4 | "private": true, 5 | "version": "0.3.1", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "expo start -w" 9 | }, 10 | "author": "650 Industries, Inc.", 11 | "license": "MIT", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "@ant-design/icons": "^5.5.1", 15 | "@babel/core": "^7.26.0", 16 | "@react-native-async-storage/async-storage": "^1.24.0", 17 | "@types/react": "~19.0.10", 18 | "antd": "^5.22.0", 19 | "expo": "53.0.7", 20 | "react": "19.0.0", 21 | "react-dom": "19.0.0", 22 | "@microlink/react-json-view": "^1.23.4", 23 | "react-native": "0.79.2", 24 | "react-native-web": "0.20.0", 25 | "typescript": "~5.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/async-storage/webui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { App } from 'antd'; 2 | 3 | import { AsyncStorageTable } from './AsyncStorageTable'; 4 | 5 | export default function Main() { 6 | return ( 7 | 8 | 9 |

10 | By default, the reload button will not update any fields you have changed. To fully update 11 | the list hold the Shift key when clicking the reload button. 12 |

13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/async-storage/webui/src/modal/useAddEntryDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, Form, Input, Modal } from 'antd'; 2 | import { ReactElement, useCallback, useState } from 'react'; 3 | 4 | export function useAddEntryDialog({ 5 | set, 6 | }: { 7 | set: (key: string, value: string) => Promise; 8 | }): { 9 | showAddEntryDialog: () => void; 10 | AddEntryDialog: ReactElement; 11 | showing: boolean; 12 | } { 13 | const [shown, setShown] = useState(false); 14 | 15 | const [key, setKey] = useState(''); 16 | const [value, setValue] = useState(''); 17 | const [emptyJson, setEmptyJson] = useState(false); 18 | const [emptyArray, setEmptyArray] = useState(false); 19 | 20 | const reset = useCallback(() => { 21 | setKey(''); 22 | setValue(''); 23 | setEmptyJson(false); 24 | setEmptyArray(false); 25 | }, [setKey, setValue, setEmptyJson, setEmptyArray]); 26 | 27 | const showAddEntryDialog = useCallback(() => { 28 | reset(); 29 | setShown(true); 30 | }, [reset, setShown]); 31 | 32 | return { 33 | showAddEntryDialog, 34 | AddEntryDialog: ( 35 |
36 | { 42 | setShown(false); 43 | reset(); 44 | }} 45 | onOk={() => { 46 | let valueToSet = value; 47 | if (emptyJson && !emptyArray) { 48 | valueToSet = '{}'; 49 | } else if (emptyArray && !emptyJson) { 50 | valueToSet = '[]'; 51 | } else if (emptyArray && emptyJson) { 52 | alert('Cannot create empty JSON object and array at the same time.'); 53 | return; 54 | } 55 | set(key, valueToSet).then(() => { 56 | setShown(false); 57 | reset(); 58 | }); 59 | }} 60 | okButtonProps={{ 61 | disabled: !key || (!value && !emptyJson && !emptyArray), 62 | }}> 63 | 64 | setKey(e.target.value)} required /> 65 | 66 | 67 | setValue(e.target.value)} 70 | required 71 | disabled={emptyJson || emptyArray} 72 | /> 73 | 74 | 75 | setEmptyJson(e.target.checked)} 78 | disabled={emptyArray}> 79 | Create empty JSON object 80 | 81 | 82 | 83 | setEmptyArray(e.target.checked)} 86 | disabled={emptyJson}> 87 | Create empty JSON array 88 | 89 | 90 | 91 |
92 | ), 93 | showing: shown, 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /packages/async-storage/webui/src/modal/useRemoveEntryModal.tsx: -------------------------------------------------------------------------------- 1 | import { App } from 'antd'; 2 | import { useCallback } from 'react'; 3 | 4 | export function useRemoveEntryModal({ remove }: { remove: (key: string) => Promise }): { 5 | showRemoveEntryModal: (key: string) => void; 6 | } { 7 | const { modal } = App.useApp(); 8 | 9 | const showRemoveEntryModal = useCallback( 10 | (key: string) => { 11 | modal.confirm({ 12 | title: 'Would you like to remove this entry?', 13 | content: `Delete ${key}?`, 14 | onOk: () => remove(key), 15 | }); 16 | }, 17 | [modal, remove] 18 | ); 19 | 20 | return { showRemoveEntryModal }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/async-storage/webui/src/useTableData.tsx: -------------------------------------------------------------------------------- 1 | // Import all the hooks 2 | import { App } from 'antd'; 3 | import { Reducer, useEffect, useReducer, useState } from 'react'; 4 | 5 | export type TableRow = { 6 | key: string; 7 | value: string; 8 | editedValue?: string; 9 | json: object | null; 10 | }; 11 | 12 | function jsonStructure(str: unknown): object | null { 13 | if (typeof str !== 'string') return null; 14 | try { 15 | const result = JSON.parse(str); 16 | const type = Object.prototype.toString.call(result); 17 | return type === '[object Object]' || type === '[object Array]' ? result : null; 18 | } catch { 19 | return null; 20 | } 21 | } 22 | 23 | export function useTableData({ 24 | entries, 25 | }: { 26 | entries: readonly { 27 | key: string; 28 | value: string | null; 29 | }[]; 30 | }) { 31 | const { message } = App.useApp(); 32 | 33 | try { 34 | const [inProgressEdits, updateInProgressEdits] = useReducer< 35 | Reducer, Record | 'clear'> 36 | >((state, payload) => { 37 | if (payload === 'clear') { 38 | return {}; 39 | } 40 | return { 41 | ...state, 42 | ...payload, 43 | }; 44 | }, {}); 45 | 46 | // eslint-disable-next-line react-hooks/rules-of-hooks 47 | const [rows, updateRows] = useState([]); 48 | 49 | // eslint-disable-next-line react-hooks/rules-of-hooks 50 | useEffect(() => { 51 | updateRows( 52 | entries.map((entry) => { 53 | const editedValue = (inProgressEdits[entry.key] || entry.value) ?? ''; 54 | return { 55 | key: entry.key, 56 | value: entry.value ?? '', 57 | editedValue, 58 | json: jsonStructure(editedValue), 59 | }; 60 | }) 61 | ); 62 | }, [entries, inProgressEdits]); 63 | 64 | return { 65 | rows, 66 | inProgressEdits, 67 | updateInProgressEdits, 68 | }; 69 | } catch (err) { 70 | console.error(err); 71 | message.error(String(err)); 72 | return { 73 | rows: [], 74 | inProgressEdits: {}, 75 | updateInProgressEdits: () => {}, 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/async-storage/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx", "../methods.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/.npmignore: -------------------------------------------------------------------------------- 1 | # @generated by expo-module-scripts 2 | 3 | # Exclude all top-level hidden directories by convention 4 | /.*/ 5 | 6 | __mocks__ 7 | __tests__ 8 | 9 | /babel.config.js 10 | /android/src/androidTest/ 11 | /android/src/test/ 12 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/README.md: -------------------------------------------------------------------------------- 1 | # create-dev-plugin 2 | 3 | ### Usage 4 | 5 | ``` 6 | yarn create dev-plugins 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | const { defineConfig } = require('eslint/config'); 3 | const baseConfig = require('expo-module-scripts/eslint.config.base'); 4 | module.exports = defineConfig([baseConfig]); 5 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-dev-plugin", 3 | "version": "0.3.1", 4 | "description": "The script to create the Expo DevTools Plugins", 5 | "main": "build", 6 | "scripts": { 7 | "build": "ncc build ./src/index.ts -o build/", 8 | "build:prod": "ncc build ./src/index.ts -o build/ --minify --no-cache --no-source-map-register", 9 | "clean": "expo-module clean", 10 | "lint": "expo-module lint", 11 | "prepare": "expo-module clean && bun run build:prod", 12 | "prepublishOnly": "expo-module prepublishOnly", 13 | "test": "expo-module test", 14 | "typecheck": "expo-module typecheck", 15 | "watch": "bun run build --watch" 16 | }, 17 | "bin": { 18 | "create-dev-plugin": "build/index.js" 19 | }, 20 | "files": [ 21 | "build", 22 | "templates/**/*" 23 | ], 24 | "homepage": "https://github.com/expo/dev-plugins/tree/main/packages/create-dev-plugin", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/expo/dev-plugins.git", 28 | "directory": "packages/create-dev-plugin" 29 | }, 30 | "keywords": [ 31 | "expo", 32 | "creator", 33 | "devtools", 34 | "plugin" 35 | ], 36 | "author": "650 Industries, Inc.", 37 | "license": "MIT", 38 | "dependencies": {}, 39 | "devDependencies": { 40 | "@expo/json-file": "^9.0.0", 41 | "@expo/package-manager": "^1.6.1", 42 | "@expo/spawn-async": "^1.7.2", 43 | "@types/ejs": "^3.1.5", 44 | "@types/getenv": "^1.0.3", 45 | "@types/prompts": "^2.4.9", 46 | "@types/validate-npm-package-name": "^4.0.2", 47 | "@vercel/ncc": "^0.38.2", 48 | "chalk": "^4.1.2", 49 | "commander": "^12.1.0", 50 | "debug": "^4.3.7", 51 | "ejs": "^3.1.10", 52 | "expo-module-scripts": "^4.1.7", 53 | "getenv": "^1.0.0", 54 | "ora": "^5.4.1", 55 | "prompts": "^2.4.2", 56 | "validate-npm-package-name": "^6.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/src/copyFilesWithTransforms.ts: -------------------------------------------------------------------------------- 1 | import ejs from 'ejs'; 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | 5 | import type { Transform } from './types'; 6 | 7 | /** 8 | * Copy a file from `srcFilePath` to `dstFilePath` and apply the transforms from `transform` to the file. 9 | */ 10 | export async function copyFileWithTransformsAsync( 11 | srcFilePath: string, 12 | dstFilePath: string, 13 | transform: Transform 14 | ) { 15 | let content = await fs.readFile(srcFilePath, 'utf8'); 16 | content = ejs.render(content, transform); 17 | 18 | let basename = path.basename(dstFilePath); 19 | basename = ejs.render(basename.replace(/^\$/, ''), transform, { 20 | openDelimiter: '{', 21 | closeDelimiter: '}', 22 | escape: (value: string) => value.replace(/\./g, path.sep), 23 | }); 24 | const targetFilePath = path.join(path.dirname(dstFilePath), basename); 25 | 26 | await fs.writeFile(targetFilePath, content, 'utf8'); 27 | const { mode } = await fs.stat(srcFilePath); 28 | await fs.chmod(targetFilePath, mode); 29 | } 30 | 31 | /** 32 | * Copy a directory from `srcDirPath` to `dstDirPath` and apply the transforms from `transform` to the files. 33 | */ 34 | export async function copyDirWithTransformsAsync( 35 | srcDirPath: string, 36 | dstDirPath: string, 37 | transform: Transform 38 | ) { 39 | await fs.mkdir(dstDirPath, { recursive: true }); 40 | 41 | const entries = await fs.readdir(srcDirPath, { withFileTypes: true }); 42 | 43 | for (const entry of entries) { 44 | const srcPath = path.join(srcDirPath, entry.name); 45 | const dstPath = path.join(dstDirPath, entry.name); 46 | 47 | if (entry.isDirectory()) { 48 | await copyDirWithTransformsAsync(srcPath, dstPath, transform); 49 | } else { 50 | await copyFileWithTransformsAsync(srcPath, dstPath, transform); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/src/createAppAdapterProject.ts: -------------------------------------------------------------------------------- 1 | import spawnAsync from '@expo/spawn-async'; 2 | import path from 'path'; 3 | 4 | import { copyDirWithTransformsAsync } from './copyFilesWithTransforms'; 5 | import { EXPO_BETA } from './env'; 6 | import { installDependenciesAsync, type PackageManagerName } from './resolvePackageManager'; 7 | import type { ProjectInfo } from './types'; 8 | 9 | const TEMPLATE_ROOT = path.join(__dirname, '..', 'templates', 'app-adapter'); 10 | 11 | const debug = require('debug')('create-dev-plugin:createAppAdapterProject') as typeof console.log; 12 | 13 | export async function createAppAdapterProjectAsync( 14 | projectRoot: string, 15 | projectInfo: ProjectInfo, 16 | packageManager: PackageManagerName 17 | ) { 18 | const packageVersions = await queryExpoPackageVersionAsync(); 19 | debug(`Using expo package versions: ${packageVersions.expoPackageVersion}`); 20 | 21 | const transform = { project: projectInfo, ...packageVersions, packageManager }; 22 | await copyDirWithTransformsAsync(TEMPLATE_ROOT, projectRoot, transform); 23 | 24 | debug(`Installing packages by ${packageManager}`); 25 | await installDependenciesAsync(projectRoot, packageManager, { silent: true }); 26 | } 27 | 28 | export async function queryExpoPackageVersionAsync(): Promise<{ 29 | expoPackageVersion: string; 30 | reactPackageVersion: string; 31 | typesReactPackageVersion: string; 32 | }> { 33 | const distTag = EXPO_BETA ? 'next' : 'latest'; 34 | const { stdout } = await spawnAsync('npm', [ 35 | 'view', 36 | `expo-template-default@${distTag}`, 37 | 'dependencies', 38 | 'devDependencies', 39 | '--json', 40 | ]); 41 | const { dependencies, devDependencies } = JSON.parse(stdout); 42 | return { 43 | expoPackageVersion: dependencies['expo'], 44 | reactPackageVersion: dependencies['react'], 45 | typesReactPackageVersion: devDependencies['@types/react'], 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/src/env.ts: -------------------------------------------------------------------------------- 1 | import getenv from 'getenv'; 2 | 3 | // FIXME: disable default BETA when SDK 50 is officially released. 4 | export const EXPO_BETA = getenv.boolish('EXPO_BETA', true); 5 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/src/resolvePackageManager.ts: -------------------------------------------------------------------------------- 1 | import * as PackageManager from '@expo/package-manager'; 2 | import { execSync } from 'child_process'; 3 | 4 | const packageJSON = require('../package.json'); 5 | const CLI_NAME = packageJSON.name; 6 | 7 | export type PackageManagerName = 'npm' | 'pnpm' | 'yarn' | 'bun'; 8 | 9 | const debug = require('debug')('expo:init:resolvePackageManager') as typeof console.log; 10 | 11 | /** Determine which package manager to use for installing dependencies based on how the process was started. */ 12 | export function resolvePackageManager(): PackageManagerName { 13 | // Attempt to detect if the user started the command using `yarn` or `pnpm` or `bun` 14 | const userAgent = process.env.npm_config_user_agent; 15 | debug('npm_config_user_agent:', userAgent); 16 | if (userAgent?.startsWith('yarn')) { 17 | return 'yarn'; 18 | } else if (userAgent?.startsWith('pnpm')) { 19 | return 'pnpm'; 20 | } else if (userAgent?.startsWith('bun')) { 21 | return 'bun'; 22 | } else if (userAgent?.startsWith('npm')) { 23 | return 'npm'; 24 | } 25 | 26 | // Try availability 27 | if (isPackageManagerAvailable('yarn')) { 28 | return 'yarn'; 29 | } else if (isPackageManagerAvailable('pnpm')) { 30 | return 'pnpm'; 31 | } else if (isPackageManagerAvailable('bun')) { 32 | return 'bun'; 33 | } 34 | 35 | return 'npm'; 36 | } 37 | 38 | export function isPackageManagerAvailable(manager: PackageManagerName): boolean { 39 | try { 40 | execSync(`${manager} --version`, { stdio: 'ignore' }); 41 | return true; 42 | } catch {} 43 | return false; 44 | } 45 | 46 | export function formatRunCommand(packageManager: PackageManagerName, cmd: string) { 47 | switch (packageManager) { 48 | case 'pnpm': 49 | return `pnpm run ${cmd}`; 50 | case 'yarn': 51 | return `yarn ${cmd}`; 52 | case 'bun': 53 | return `bun run ${cmd}`; 54 | case 'npm': 55 | default: 56 | return `npm run ${cmd}`; 57 | } 58 | } 59 | 60 | export function formatSelfCommand() { 61 | const packageManager = resolvePackageManager(); 62 | switch (packageManager) { 63 | case 'pnpm': 64 | return `pnpx ${CLI_NAME}`; 65 | case 'bun': 66 | return `bunx ${CLI_NAME}`; 67 | case 'yarn': 68 | case 'npm': 69 | default: 70 | return `npx ${CLI_NAME}`; 71 | } 72 | } 73 | 74 | export async function installDependenciesAsync( 75 | projectRoot: string, 76 | packageManager: PackageManagerName, 77 | flags: { silent: boolean } = { silent: false } 78 | ) { 79 | const options = { cwd: projectRoot, silent: flags.silent }; 80 | if (packageManager === 'yarn') { 81 | await new PackageManager.YarnPackageManager(options).installAsync(); 82 | } else if (packageManager === 'pnpm') { 83 | await new PackageManager.PnpmPackageManager(options).installAsync(); 84 | } else if (packageManager === 'bun') { 85 | await new PackageManager.BunPackageManager(options).installAsync(); 86 | } else { 87 | await new PackageManager.NpmPackageManager(options).installAsync(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ProjectInfo { 2 | name: string; 3 | description: string; 4 | hookName: string; 5 | } 6 | 7 | export interface Transform { 8 | project: ProjectInfo; 9 | 10 | /** The `expo` package version to install */ 11 | expoPackageVersion: string; 12 | } 13 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/src/utils.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import ora from 'ora'; 3 | 4 | export type StepOptions = ora.Options; 5 | 6 | export async function newStep( 7 | title: string, 8 | action: (step: ora.Ora) => Promise | Result, 9 | options: StepOptions = {} 10 | ): Promise { 11 | const disabled = process.env.CI || process.env.EXPO_DEBUG; 12 | const step = ora({ 13 | text: chalk.bold(title), 14 | isEnabled: !disabled, 15 | stream: disabled ? process.stdout : process.stderr, 16 | ...options, 17 | }); 18 | 19 | step.start(); 20 | 21 | try { 22 | return await action(step); 23 | } catch (error) { 24 | step.fail(); 25 | console.error(error); 26 | process.exit(1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/$.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/babel.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/babel.config.base'); 3 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | const { defineConfig } = require('eslint/config'); 3 | const baseConfig = require('expo-module-scripts/eslint.config.base'); 4 | module.exports = defineConfig([baseConfig]); 5 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["devtools"], 3 | "devtools": { 4 | "webpageRoot": "dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%- project.name %>", 3 | "version": "0.1.0", 4 | "description": "<%- project.description %>", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "expo-module build", 10 | "build:all": "expo-module prepare && <%- packageManager %> run web:export", 11 | "clean": "expo-module clean", 12 | "prepare": "expo-module prepare", 13 | "prepublishOnly": "expo-module prepare && expo-module prepublishOnly && <%- packageManager %> run web:export", 14 | "web:dev": "cd webui && npx expo start -w", 15 | "web:export": "./scripts/build-webui.js" 16 | }, 17 | "keywords": [ 18 | "expo", 19 | "devtools" 20 | ], 21 | "files": [ 22 | "build", 23 | "dist", 24 | "expo-module.config.json" 25 | ], 26 | "license": "MIT", 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "@types/react": "<%- typesReactPackageVersion %>", 30 | "expo": "<%- expoPackageVersion %>", 31 | "expo-module-scripts": "^4.1.7", 32 | "react": "<%- reactPackageVersion %>", 33 | "typescript": "~5.8.3" 34 | }, 35 | "peerDependencies": { 36 | "expo": "*" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/scripts/build-webui.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawn } = require('child_process'); 4 | const fs = require('fs/promises'); 5 | const path = require('path'); 6 | 7 | const projectRoot = path.resolve(__dirname, '..'); 8 | 9 | async function runAsync() { 10 | await spawnAsync('npx', ['expo', 'export', '-p', 'web', '--output-dir', 'dist'], { 11 | cwd: path.join(projectRoot, 'webui'), 12 | }); 13 | 14 | // [1] Remove dist if it exists 15 | const distPath = path.join(projectRoot, 'dist'); 16 | try { 17 | await fs.rm(distPath, { recursive: true, force: true }); 18 | } catch {} 19 | 20 | // [2] Move dist from webui 21 | const srcDist = path.join(projectRoot, 'webui', 'dist'); 22 | await fs.rename(srcDist, distPath); 23 | } 24 | 25 | async function spawnAsync(command, args, options = {}) { 26 | return new Promise((resolve, reject) => { 27 | const child = spawn(command, args, { stdio: 'inherit', shell: true, ...options }); 28 | child.on('close', (code) => { 29 | if (code !== 0) { 30 | reject(new Error(`${command} process exited with code ${code}`)); 31 | } else { 32 | resolve(); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | (async () => { 39 | try { 40 | await runAsync(); 41 | } catch (error) { 42 | console.error('Error during build-webui:', error); 43 | process.exit(1); 44 | } 45 | })(); 46 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/src/index.ts: -------------------------------------------------------------------------------- 1 | export let <%= project.hookName %>: typeof import('./<%= project.hookName %>').<%= project.hookName %>; 2 | 3 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 4 | if (process.env.NODE_ENV !== 'production') { 5 | <%= project.hookName %> = require('./<%= project.hookName %>').<%= project.hookName %>; 6 | } else { 7 | <%= project.hookName %> = () => {}; 8 | } 9 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/src/{%= project.hookName %}.ts: -------------------------------------------------------------------------------- 1 | import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 2 | import { useEffect } from 'react'; 3 | 4 | export function <%= project.hookName %>() { 5 | const client = useDevToolsPluginClient('<%- project.name %>'); 6 | 7 | useEffect(() => { 8 | const subscriptions: (EventSubscription | undefined)[] = []; 9 | 10 | subscriptions.push( 11 | client?.addMessageListener('ping', (data) => { 12 | alert(`Received ping from ${data.from}`); 13 | }) 14 | ); 15 | client?.sendMessage('ping', { from: 'app' }); 16 | 17 | return () => { 18 | for (const subscription of subscriptions) { 19 | subscription?.remove(); 20 | } 21 | }; 22 | }, [client]); 23 | } 24 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/templates/app-adapter/tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/create-dev-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.node", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "rootDir": "src", 6 | }, 7 | "include": ["./src"], 8 | "exclude": [ 9 | "**/__mocks__/*", 10 | "**/__tests__/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/README.md: -------------------------------------------------------------------------------- 1 | # @dev-plugins/react-native-mmkv 2 | 3 | A React Native MMKV DevTool that can run in an Expo App 4 | 5 | # Installation 6 | 7 | ### Add the package to your project 8 | 9 | ``` 10 | npx expo install @dev-plugins/react-native-mmkv 11 | ``` 12 | 13 | ### Integrate react-native-mmkv with the DevTool hook 14 | 15 | ```jsx 16 | import { useMMKVDevTools } from '@dev-plugins/react-native-mmkv'; 17 | import { MMKV } from 'react-native-mmkv'; 18 | 19 | const yourMmkvStorage = new MMKV({ id: 'any_id' }); 20 | 21 | export default function App() { 22 | useMMKVDevTools({ storage: yourMmkvStorage }); 23 | /* ... */ 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/babel.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/babel.config.base'); 3 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/build/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare let useMMKVDevTools: typeof import('./useMMKVDevTools').useMMKVDevTools; 2 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /packages/react-native-mmkv/build/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,IAAI,eAAe,EAAE,cAAc,mBAAmB,EAAE,eAAe,CAAC"} -------------------------------------------------------------------------------- /packages/react-native-mmkv/build/index.js: -------------------------------------------------------------------------------- 1 | export let useMMKVDevTools; 2 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 3 | if (process.env.NODE_ENV !== 'production') { 4 | useMMKVDevTools = require('./useMMKVDevTools').useMMKVDevTools; 5 | } 6 | else { 7 | useMMKVDevTools = () => { }; 8 | } 9 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /packages/react-native-mmkv/build/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,IAAI,eAAmE,CAAC;AAE/E,wEAAwE;AACxE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,eAAe,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,eAAe,CAAC;AACjE,CAAC;KAAM,CAAC;IACN,eAAe,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAC7B,CAAC","sourcesContent":["export let useMMKVDevTools: typeof import('./useMMKVDevTools').useMMKVDevTools;\n\n// @ts-ignore process.env.NODE_ENV is defined by metro transform plugins\nif (process.env.NODE_ENV !== 'production') {\n useMMKVDevTools = require('./useMMKVDevTools').useMMKVDevTools;\n} else {\n useMMKVDevTools = () => {};\n}\n"]} -------------------------------------------------------------------------------- /packages/react-native-mmkv/build/useMMKVDevTools.d.ts: -------------------------------------------------------------------------------- 1 | import { MMKV } from 'react-native-mmkv'; 2 | /** 3 | * This hook registers a devtools plugin for react-native-mmkv. 4 | * 5 | * The plugin provides you with the ability to view, add, edit, and remove react-native-mmkv entries. 6 | * 7 | * @param props 8 | * @param props.errorHandler - A function that will be called with any errors that occur while communicating 9 | * with the devtools, if not provided errors will be ignored. Setting this is highly recommended. 10 | * @param props.storage - A MMKV storage instance to use, if not provided the default storage will be used. 11 | */ 12 | export declare function useMMKVDevTools({ errorHandler, storage, }?: { 13 | errorHandler?: (error: Error) => void; 14 | storage?: MMKV; 15 | }): void; 16 | //# sourceMappingURL=useMMKVDevTools.d.ts.map -------------------------------------------------------------------------------- /packages/react-native-mmkv/build/useMMKVDevTools.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"useMMKVDevTools.d.ts","sourceRoot":"","sources":["../src/useMMKVDevTools.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAIzC;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,EAC9B,YAAY,EACZ,OAAoB,GACrB,GAAE;IACD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,IAAI,CAAC;CACX,QAiFL"} -------------------------------------------------------------------------------- /packages/react-native-mmkv/build/useMMKVDevTools.js: -------------------------------------------------------------------------------- 1 | import { useDevToolsPluginClient } from 'expo/devtools'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { MMKV } from 'react-native-mmkv'; 4 | /** 5 | * This hook registers a devtools plugin for react-native-mmkv. 6 | * 7 | * The plugin provides you with the ability to view, add, edit, and remove react-native-mmkv entries. 8 | * 9 | * @param props 10 | * @param props.errorHandler - A function that will be called with any errors that occur while communicating 11 | * with the devtools, if not provided errors will be ignored. Setting this is highly recommended. 12 | * @param props.storage - A MMKV storage instance to use, if not provided the default storage will be used. 13 | */ 14 | export function useMMKVDevTools({ errorHandler, storage = new MMKV(), } = {}) { 15 | const client = useDevToolsPluginClient('mmkv'); 16 | const handleError = useCallback((error) => { 17 | if (error instanceof Error) { 18 | errorHandler?.(error); 19 | } 20 | else { 21 | errorHandler?.(new Error(`Unknown error: ${String(error)}`)); 22 | } 23 | }, [errorHandler]); 24 | useEffect(() => { 25 | const on = (event, listener) => client?.addMessageListener(event, async (params) => { 26 | try { 27 | const result = await listener(params); 28 | client?.sendMessage(`ack:${event}`, { result }); 29 | } 30 | catch (error) { 31 | try { 32 | client?.sendMessage('error', { error }); 33 | handleError(error); 34 | } 35 | catch (e) { 36 | handleError(e); 37 | } 38 | } 39 | }); 40 | const subscriptions = []; 41 | try { 42 | subscriptions.push(on('getAll', async () => { 43 | const keys = storage.getAllKeys(); 44 | return keys?.map((key) => [key, storage.getString(key)]); 45 | })); 46 | } 47 | catch (e) { 48 | handleError(e); 49 | } 50 | try { 51 | subscriptions.push(on('set', async ({ key, value }) => { 52 | if (key !== undefined && value !== undefined) { 53 | return storage.set(key, value); 54 | } 55 | })); 56 | } 57 | catch (e) { 58 | handleError(e); 59 | } 60 | try { 61 | subscriptions.push(on('remove', async ({ key }) => { 62 | if (key !== undefined) { 63 | storage.delete(key); 64 | } 65 | })); 66 | } 67 | catch (e) { 68 | handleError(e); 69 | } 70 | return () => { 71 | for (const subscription of subscriptions) { 72 | try { 73 | subscription?.remove(); 74 | } 75 | catch (e) { 76 | handleError(e); 77 | } 78 | } 79 | }; 80 | }, [client]); 81 | } 82 | //# sourceMappingURL=useMMKVDevTools.js.map -------------------------------------------------------------------------------- /packages/react-native-mmkv/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | const { defineConfig } = require('eslint/config'); 3 | const baseConfig = require('expo-module-scripts/eslint.config.base'); 4 | module.exports = defineConfig([baseConfig]); 5 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["devtools"], 3 | "devtools": { 4 | "webpageRoot": "dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/methods.d.ts: -------------------------------------------------------------------------------- 1 | export type Method = 'getAll' | 'set' | 'remove'; 2 | export type MethodAck = `ack:${Method}`; 3 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/react-native-mmkv", 3 | "version": "0.3.1", 4 | "description": "Expo DevTools Plugin for react-native-mmkv", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "expo-module build", 10 | "clean": "expo-module clean", 11 | "lint": "expo-module lint", 12 | "test": "expo-module test", 13 | "prepare": "expo-module prepare && node ../../scripts/build-webui.js react-native-mmkv", 14 | "prepublishOnly": "expo-module prepublishOnly", 15 | "expo-module": "expo-module" 16 | }, 17 | "homepage": "https://docs.expo.dev/versions/latest/sdk/image/", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/expo/dev-plugins.git", 21 | "directory": "packages/react-native-mmkv" 22 | }, 23 | "keywords": [ 24 | "expo", 25 | "devtools", 26 | "mmkv" 27 | ], 28 | "files": [ 29 | "build", 30 | "dist", 31 | "expo-module.config.json", 32 | "src" 33 | ], 34 | "author": "cyrilbo", 35 | "license": "MIT", 36 | "devDependencies": { 37 | "expo": "53.0.7", 38 | "expo-module-scripts": "^4.1.7", 39 | "react-native-mmkv": "^3.1.0", 40 | "typescript": "~5.8.3" 41 | }, 42 | "peerDependencies": { 43 | "react-native-mmkv": "*", 44 | "expo": "^53.0.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/src/index.ts: -------------------------------------------------------------------------------- 1 | export let useMMKVDevTools: typeof import('./useMMKVDevTools').useMMKVDevTools; 2 | 3 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 4 | if (process.env.NODE_ENV !== 'production') { 5 | useMMKVDevTools = require('./useMMKVDevTools').useMMKVDevTools; 6 | } else { 7 | useMMKVDevTools = () => {}; 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/src/useMMKVDevTools.ts: -------------------------------------------------------------------------------- 1 | import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { MMKV } from 'react-native-mmkv'; 4 | 5 | import { Method } from '../methods'; 6 | 7 | /** 8 | * This hook registers a devtools plugin for react-native-mmkv. 9 | * 10 | * The plugin provides you with the ability to view, add, edit, and remove react-native-mmkv entries. 11 | * 12 | * @param props 13 | * @param props.errorHandler - A function that will be called with any errors that occur while communicating 14 | * with the devtools, if not provided errors will be ignored. Setting this is highly recommended. 15 | * @param props.storage - A MMKV storage instance to use, if not provided the default storage will be used. 16 | */ 17 | export function useMMKVDevTools({ 18 | errorHandler, 19 | storage = new MMKV(), 20 | }: { 21 | errorHandler?: (error: Error) => void; 22 | storage?: MMKV; 23 | } = {}) { 24 | const client = useDevToolsPluginClient('mmkv'); 25 | 26 | const handleError = useCallback( 27 | (error: unknown) => { 28 | if (error instanceof Error) { 29 | errorHandler?.(error); 30 | } else { 31 | errorHandler?.(new Error(`Unknown error: ${String(error)}`)); 32 | } 33 | }, 34 | [errorHandler] 35 | ); 36 | 37 | useEffect(() => { 38 | const on = ( 39 | event: Method, 40 | listener: (params: { key?: string; value?: string }) => Promise 41 | ) => 42 | client?.addMessageListener(event, async (params: { key?: string; value?: string }) => { 43 | try { 44 | const result = await listener(params); 45 | 46 | client?.sendMessage(`ack:${event}`, { result }); 47 | } catch (error) { 48 | try { 49 | client?.sendMessage('error', { error }); 50 | handleError(error); 51 | } catch (e) { 52 | handleError(e); 53 | } 54 | } 55 | }); 56 | 57 | const subscriptions: (EventSubscription | undefined)[] = []; 58 | 59 | try { 60 | subscriptions.push( 61 | on('getAll', async () => { 62 | const keys = storage.getAllKeys(); 63 | return keys?.map((key) => [key, storage.getString(key)]); 64 | }) 65 | ); 66 | } catch (e) { 67 | handleError(e); 68 | } 69 | 70 | try { 71 | subscriptions.push( 72 | on('set', async ({ key, value }) => { 73 | if (key !== undefined && value !== undefined) { 74 | return storage.set(key, value); 75 | } 76 | }) 77 | ); 78 | } catch (e) { 79 | handleError(e); 80 | } 81 | 82 | try { 83 | subscriptions.push( 84 | on('remove', async ({ key }) => { 85 | if (key !== undefined) { 86 | storage.delete(key); 87 | } 88 | }) 89 | ); 90 | } catch (e) { 91 | handleError(e); 92 | } 93 | 94 | return () => { 95 | for (const subscription of subscriptions) { 96 | try { 97 | subscription?.remove(); 98 | } catch (e) { 99 | handleError(e); 100 | } 101 | } 102 | }; 103 | }, [client]); 104 | } 105 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "web": { 4 | "bundler": "metro" 5 | }, 6 | "experiments": { 7 | "baseUrl": "/_expo/plugins/@dev-plugins/react-native-mmkv" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/react-native-mmkv-webui", 3 | "description": "The frontend webui for @dev-plugins/react-native-mmkv", 4 | "private": true, 5 | "version": "0.3.1", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "expo start -w" 9 | }, 10 | "author": "650 Industries, Inc.", 11 | "license": "MIT", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "@ant-design/icons": "^5.5.1", 15 | "@babel/core": "^7.26.0", 16 | "@types/react": "~19.0.10", 17 | "antd": "^5.22.0", 18 | "expo": "53.0.7", 19 | "react": "19.0.0", 20 | "react-dom": "19.0.0", 21 | "@microlink/react-json-view": "^1.23.4", 22 | "react-native": "0.79.2", 23 | "react-native-web": "0.20.0", 24 | "typescript": "~5.8.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Spin } from 'antd'; 2 | 3 | import { MMKVStorageTable } from './MMKVStorageTable'; 4 | import { useConnectedClient } from './useConnectedClient'; 5 | 6 | export default function App() { 7 | const connectedClient = useConnectedClient(); 8 | 9 | if (!connectedClient) { 10 | return ; 11 | } 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/src/MMKVStorageTable.tsx: -------------------------------------------------------------------------------- 1 | import { App } from 'antd'; 2 | import { DevToolsPluginClient } from 'expo/devtools'; 3 | import { useEffect, useState } from 'react'; 4 | 5 | import { KeyValueStorageTable } from './KeyValueStorageTable'; 6 | import { usePluginStore } from './usePluginStore'; 7 | 8 | export const MMKVStorageTable = ({ client }: { client: DevToolsPluginClient }) => { 9 | const { message } = App.useApp(); 10 | const { entries, update, set, remove } = usePluginStore(client, (error: unknown) => { 11 | message.error(String(error)); 12 | console.error(error); 13 | }); 14 | 15 | const [initialUpdate, setInitialUpdate] = useState(false); 16 | useEffect(() => { 17 | if (!initialUpdate) { 18 | update() 19 | .then(() => setInitialUpdate(true)) 20 | .catch(console.error); 21 | } 22 | }, [initialUpdate]); 23 | 24 | return ( 25 | 26 | 27 |

28 | By default, the reload button will not update any fields you have changed. To fully update 29 | the list hold the Shift key when clicking the reload button. 30 |

31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/src/modal/useAddEntryDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, Form, Input, Modal } from 'antd'; 2 | import { ReactElement, useCallback, useState } from 'react'; 3 | 4 | export function useAddEntryDialog({ 5 | set, 6 | }: { 7 | set: (key: string, value: string) => Promise; 8 | }): { 9 | showAddEntryDialog: () => void; 10 | AddEntryDialog: ReactElement; 11 | showing: boolean; 12 | } { 13 | const [shown, setShown] = useState(false); 14 | 15 | const [key, setKey] = useState(''); 16 | const [value, setValue] = useState(''); 17 | const [emptyJson, setEmptyJson] = useState(false); 18 | const [emptyArray, setEmptyArray] = useState(false); 19 | 20 | const reset = useCallback(() => { 21 | setKey(''); 22 | setValue(''); 23 | setEmptyJson(false); 24 | setEmptyArray(false); 25 | }, [setKey, setValue, setEmptyJson, setEmptyArray]); 26 | 27 | const showAddEntryDialog = useCallback(() => { 28 | reset(); 29 | setShown(true); 30 | }, [reset, setShown]); 31 | 32 | return { 33 | showAddEntryDialog, 34 | AddEntryDialog: ( 35 |
36 | { 42 | setShown(false); 43 | reset(); 44 | }} 45 | onOk={() => { 46 | let valueToSet = value; 47 | if (emptyJson && !emptyArray) { 48 | valueToSet = '{}'; 49 | } else if (emptyArray && !emptyJson) { 50 | valueToSet = '[]'; 51 | } else if (emptyArray && emptyJson) { 52 | alert('Cannot create empty JSON object and array at the same time.'); 53 | return; 54 | } 55 | set(key, valueToSet).then(() => { 56 | setShown(false); 57 | reset(); 58 | }); 59 | }} 60 | okButtonProps={{ 61 | disabled: !key || (!value && !emptyJson && !emptyArray), 62 | }}> 63 | 64 | setKey(e.target.value)} required /> 65 | 66 | 67 | setValue(e.target.value)} 70 | required 71 | disabled={emptyJson || emptyArray} 72 | /> 73 | 74 | 75 | setEmptyJson(e.target.checked)} 78 | disabled={emptyArray}> 79 | Create empty JSON object 80 | 81 | 82 | 83 | setEmptyArray(e.target.checked)} 86 | disabled={emptyJson}> 87 | Create empty JSON array 88 | 89 | 90 | 91 |
92 | ), 93 | showing: shown, 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/src/modal/useRemoveEntryModal.tsx: -------------------------------------------------------------------------------- 1 | import { App } from 'antd'; 2 | import { useCallback } from 'react'; 3 | 4 | export function useRemoveEntryModal({ remove }: { remove: (key: string) => Promise }): { 5 | showRemoveEntryModal: (key: string) => void; 6 | } { 7 | const { modal } = App.useApp(); 8 | 9 | const showRemoveEntryModal = useCallback( 10 | (key: string) => { 11 | modal.confirm({ 12 | title: 'Would you like to remove this entry?', 13 | content: `Delete ${key}?`, 14 | onOk: () => remove(key), 15 | }); 16 | }, 17 | [modal, remove] 18 | ); 19 | 20 | return { showRemoveEntryModal }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/src/useConnectedClient.ts: -------------------------------------------------------------------------------- 1 | import { DevToolsPluginClient, useDevToolsPluginClient } from 'expo/devtools'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export const useConnectedClient = (): DevToolsPluginClient | null => { 5 | const client = useDevToolsPluginClient('mmkv'); 6 | const [status, setStatus] = useState<'connected' | 'disconnected'>('disconnected'); 7 | useEffect(() => { 8 | const interval = setInterval(() => { 9 | if (client && client.isConnected()) { 10 | setStatus('connected'); 11 | } else { 12 | setStatus('disconnected'); 13 | } 14 | }, 1000); 15 | return () => { 16 | clearInterval(interval); 17 | }; 18 | }, [client]); 19 | if (client && status === 'connected') return client; 20 | return null; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/src/usePluginStore.ts: -------------------------------------------------------------------------------- 1 | import { DevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | 4 | import { Method, MethodAck } from '../../methods'; 5 | 6 | const methodAck: Record = { 7 | getAll: 'ack:getAll', 8 | set: 'ack:set', 9 | remove: 'ack:remove', 10 | }; 11 | 12 | type KeyType = string; 13 | type ValueType = string; 14 | type KeyValuePair = [KeyType, ValueType]; 15 | 16 | export function usePluginStore(client: DevToolsPluginClient, onError: (error: unknown) => void) { 17 | const [entries, setEntries] = useState([]); 18 | 19 | const update = useCallback(async () => { 20 | try { 21 | return client.sendMessage('getAll', {}); 22 | } catch (e) { 23 | onError(e); 24 | } 25 | }, [client]); 26 | 27 | const set = useCallback( 28 | async (key: string, value: string) => { 29 | try { 30 | return client.sendMessage('set', { 31 | key, 32 | value, 33 | }); 34 | } catch (e) { 35 | onError(e); 36 | } 37 | }, 38 | [client] 39 | ); 40 | 41 | const remove = useCallback( 42 | async (key: string) => { 43 | try { 44 | return client.sendMessage('remove', { 45 | key, 46 | }); 47 | } catch (e) { 48 | onError(e); 49 | } 50 | }, 51 | [client] 52 | ); 53 | 54 | useEffect(() => { 55 | const subscriptions: (EventSubscription | undefined)[] = []; 56 | try { 57 | subscriptions.push( 58 | client.addMessageListener( 59 | methodAck.getAll, 60 | ({ result }: { result: readonly KeyValuePair[] }) => { 61 | setEntries(result.map(([key, value]) => ({ key, value }))); 62 | } 63 | ) 64 | ); 65 | } catch (e) { 66 | onError(e); 67 | } 68 | 69 | try { 70 | subscriptions.push( 71 | client.addMessageListener(methodAck.set, () => { 72 | update(); 73 | }) 74 | ); 75 | } catch (e) { 76 | onError(e); 77 | } 78 | 79 | try { 80 | subscriptions.push( 81 | client.addMessageListener(methodAck.remove, () => { 82 | update(); 83 | }) 84 | ); 85 | } catch (e) { 86 | onError(e); 87 | } 88 | 89 | subscriptions.push( 90 | client.addMessageListener('error', ({ error }: { error: unknown }) => { 91 | onError(error); 92 | }) 93 | ); 94 | 95 | return () => { 96 | for (const subscription of subscriptions) { 97 | try { 98 | subscription?.remove(); 99 | } catch (e) { 100 | onError(e); 101 | } 102 | } 103 | }; 104 | }, [client]); 105 | 106 | return { 107 | entries, 108 | update, 109 | set, 110 | remove, 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/src/useTableData.tsx: -------------------------------------------------------------------------------- 1 | // Import all the hooks 2 | import { App } from 'antd'; 3 | import { Reducer, useEffect, useReducer, useState } from 'react'; 4 | 5 | export type TableRow = { 6 | key: string; 7 | value: string; 8 | editedValue?: string; 9 | json: object | null; 10 | }; 11 | 12 | function jsonStructure(str: unknown): object | null { 13 | if (typeof str !== 'string') return null; 14 | try { 15 | const result = JSON.parse(str); 16 | const type = Object.prototype.toString.call(result); 17 | return type === '[object Object]' || type === '[object Array]' ? result : null; 18 | } catch { 19 | return null; 20 | } 21 | } 22 | 23 | export function useTableData({ 24 | entries, 25 | }: { 26 | entries: readonly { 27 | key: string; 28 | value: string | null; 29 | }[]; 30 | }) { 31 | const { message } = App.useApp(); 32 | try { 33 | const [inProgressEdits, updateInProgressEdits] = useReducer< 34 | Reducer, Record | 'clear'> 35 | >((state, payload) => { 36 | if (payload === 'clear') { 37 | return {}; 38 | } 39 | return { 40 | ...state, 41 | ...payload, 42 | }; 43 | }, {}); 44 | 45 | // eslint-disable-next-line react-hooks/rules-of-hooks 46 | const [rows, updateRows] = useState([]); 47 | 48 | // eslint-disable-next-line react-hooks/rules-of-hooks 49 | useEffect(() => { 50 | updateRows( 51 | entries.map((entry) => { 52 | const editedValue = (inProgressEdits[entry.key] || entry.value) ?? ''; 53 | return { 54 | key: entry.key, 55 | value: entry.value ?? '', 56 | editedValue, 57 | json: jsonStructure(editedValue), 58 | }; 59 | }) 60 | ); 61 | }, [entries, inProgressEdits]); 62 | 63 | return { 64 | rows, 65 | inProgressEdits, 66 | updateInProgressEdits, 67 | }; 68 | } catch (err) { 69 | console.error(err); 70 | message.error(String(err)); 71 | return { 72 | rows: [], 73 | inProgressEdits: {}, 74 | updateInProgressEdits: () => {}, 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/react-native-mmkv/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx", "../methods.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-navigation/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /packages/react-navigation/README.md: -------------------------------------------------------------------------------- 1 | # @dev-plugins/react-navigation 2 | 3 | A React Navigation DevTool that can run in an Expo App 4 | 5 | ## Installation 6 | 7 | ### Add the package to your project 8 | 9 | ```bash 10 | npx expo install @dev-plugins/react-navigation 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Using with `react-navigation` 16 | 17 | #### Integrate `react-navigation` with the DevTool hook 18 | 19 | ```jsx 20 | import { useNavigationContainerRef } from '@react-navigation/native'; 21 | import { useReactNavigationDevTools } from '@dev-plugins/react-navigation'; 22 | 23 | export default function App() { 24 | const navigationRef = useNavigationContainerRef(); 25 | useReactNavigationDevTools(navigationRef); 26 | 27 | return {/* ... */}; 28 | } 29 | ``` 30 | 31 | ### Using with `expo-router` 32 | 33 | When using `expo-router`, integrate the DevTool in your main `_layout.tsx` file. You can import `useNavigationContainerRef` directly from `expo-router` and pass it to `useReactNavigationDevTools`: 34 | 35 | ```tsx 36 | import { useNavigationContainerRef } from 'expo-router'; 37 | import { useReactNavigationDevTools } from '@dev-plugins/react-navigation'; 38 | 39 | export default function RootLayout() { 40 | const navigationRef = useNavigationContainerRef(); 41 | useReactNavigationDevTools(navigationRef); 42 | 43 | return 44 | } 45 | ``` 46 | 47 | In this case, `expo-router` automatically manages the navigation container, so you just need to add the DevTool setup in your layout component. 48 | -------------------------------------------------------------------------------- /packages/react-navigation/babel.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/babel.config.base'); 3 | -------------------------------------------------------------------------------- /packages/react-navigation/build/ReduxExtensionAdapter.d.ts: -------------------------------------------------------------------------------- 1 | import { type DevToolsPluginClient } from 'expo/devtools'; 2 | type ReduxExtensionEventListener = (message: { 3 | type: string; 4 | [key: string]: any; 5 | }) => void; 6 | /** 7 | * A stub Redux extension connector for `useReduxDevToolsExtension` 8 | */ 9 | export declare class ReduxExtensionAdapter { 10 | private client; 11 | private reduxExtensionListener; 12 | init(value: any): void; 13 | send(action: any, state: any): void; 14 | subscribe(reduxExtensionListener: ReduxExtensionEventListener): void; 15 | resetRoot(value: any): void; 16 | setClient(client: DevToolsPluginClient | null): void; 17 | } 18 | export {}; 19 | //# sourceMappingURL=ReduxExtensionAdapter.d.ts.map -------------------------------------------------------------------------------- /packages/react-navigation/build/ReduxExtensionAdapter.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ReduxExtensionAdapter.d.ts","sourceRoot":"","sources":["../src/ReduxExtensionAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAG1D,KAAK,2BAA2B,GAAG,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,KAAK,IAAI,CAAC;AAE3F;;GAEG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,sBAAsB,CAA4C;IAE1E,IAAI,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI;IAOtB,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAQnC,SAAS,CAAC,sBAAsB,EAAE,2BAA2B;IAI7D,SAAS,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI;IAI3B,SAAS,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI;CAG9C"} -------------------------------------------------------------------------------- /packages/react-navigation/build/ReduxExtensionAdapter.js: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid/non-secure'; 2 | /** 3 | * A stub Redux extension connector for `useReduxDevToolsExtension` 4 | */ 5 | export class ReduxExtensionAdapter { 6 | client = null; 7 | reduxExtensionListener = null; 8 | init(value) { 9 | this.client?.sendMessage('init', { 10 | id: nanoid(), 11 | value, 12 | }); 13 | } 14 | send(action, state) { 15 | this.client?.sendMessage('action', { 16 | id: nanoid(), 17 | action, 18 | state, 19 | }); 20 | } 21 | subscribe(reduxExtensionListener) { 22 | this.reduxExtensionListener = reduxExtensionListener; 23 | } 24 | resetRoot(value) { 25 | this.reduxExtensionListener?.({ type: 'DISPATCH', state: JSON.stringify(value) }); 26 | } 27 | setClient(client) { 28 | this.client = client; 29 | } 30 | } 31 | //# sourceMappingURL=ReduxExtensionAdapter.js.map -------------------------------------------------------------------------------- /packages/react-navigation/build/ReduxExtensionAdapter.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ReduxExtensionAdapter.js","sourceRoot":"","sources":["../src/ReduxExtensionAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAI3C;;GAEG;AACH,MAAM,OAAO,qBAAqB;IACxB,MAAM,GAAgC,IAAI,CAAC;IAC3C,sBAAsB,GAAuC,IAAI,CAAC;IAE1E,IAAI,CAAC,KAAU;QACb,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE;YAC/B,EAAE,EAAE,MAAM,EAAE;YACZ,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,MAAW,EAAE,KAAU;QAC1B,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,EAAE;YACjC,EAAE,EAAE,MAAM,EAAE;YACZ,MAAM;YACN,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED,SAAS,CAAC,sBAAmD;QAC3D,IAAI,CAAC,sBAAsB,GAAG,sBAAsB,CAAC;IACvD,CAAC;IAED,SAAS,CAAC,KAAU;QAClB,IAAI,CAAC,sBAAsB,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACpF,CAAC;IAED,SAAS,CAAC,MAAmC;QAC3C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF","sourcesContent":["import { type DevToolsPluginClient } from 'expo/devtools';\nimport { nanoid } from 'nanoid/non-secure';\n\ntype ReduxExtensionEventListener = (message: { type: string; [key: string]: any }) => void;\n\n/**\n * A stub Redux extension connector for `useReduxDevToolsExtension`\n */\nexport class ReduxExtensionAdapter {\n private client: DevToolsPluginClient | null = null;\n private reduxExtensionListener: ReduxExtensionEventListener | null = null;\n\n init(value: any): void {\n this.client?.sendMessage('init', {\n id: nanoid(),\n value,\n });\n }\n\n send(action: any, state: any): void {\n this.client?.sendMessage('action', {\n id: nanoid(),\n action,\n state,\n });\n }\n\n subscribe(reduxExtensionListener: ReduxExtensionEventListener) {\n this.reduxExtensionListener = reduxExtensionListener;\n }\n\n resetRoot(value: any): void {\n this.reduxExtensionListener?.({ type: 'DISPATCH', state: JSON.stringify(value) });\n }\n\n setClient(client: DevToolsPluginClient | null) {\n this.client = client;\n }\n}\n"]} -------------------------------------------------------------------------------- /packages/react-navigation/build/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare let useReactNavigationDevTools: typeof import('./useReactNavigationDevTools').useReactNavigationDevTools; 2 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /packages/react-navigation/build/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,IAAI,0BAA0B,EAAE,cAAc,8BAA8B,EAAE,0BAA0B,CAAC"} -------------------------------------------------------------------------------- /packages/react-navigation/build/index.js: -------------------------------------------------------------------------------- 1 | export let useReactNavigationDevTools; 2 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 3 | if (process.env.NODE_ENV !== 'production') { 4 | useReactNavigationDevTools = require('./useReactNavigationDevTools').useReactNavigationDevTools; 5 | } 6 | else { 7 | useReactNavigationDevTools = () => { }; 8 | } 9 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /packages/react-navigation/build/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,IAAI,0BAAoG,CAAC;AAEhH,wEAAwE;AACxE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,0BAA0B,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAAC,0BAA0B,CAAC;AAClG,CAAC;KAAM,CAAC;IACN,0BAA0B,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AACxC,CAAC","sourcesContent":["export let useReactNavigationDevTools: typeof import('./useReactNavigationDevTools').useReactNavigationDevTools;\n\n// @ts-ignore process.env.NODE_ENV is defined by metro transform plugins\nif (process.env.NODE_ENV !== 'production') {\n useReactNavigationDevTools = require('./useReactNavigationDevTools').useReactNavigationDevTools;\n} else {\n useReactNavigationDevTools = () => {};\n}\n"]} -------------------------------------------------------------------------------- /packages/react-navigation/build/useReactNavigationDevTools.d.ts: -------------------------------------------------------------------------------- 1 | import type { NavigationContainerRef } from '@react-navigation/core'; 2 | export declare function useReactNavigationDevTools(ref: React.RefObject>): void; 3 | //# sourceMappingURL=useReactNavigationDevTools.d.ts.map -------------------------------------------------------------------------------- /packages/react-navigation/build/useReactNavigationDevTools.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"useReactNavigationDevTools.d.ts","sourceRoot":"","sources":["../src/useReactNavigationDevTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAOrE,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC,QAoE3F"} -------------------------------------------------------------------------------- /packages/react-navigation/build/useReactNavigationDevTools.js: -------------------------------------------------------------------------------- 1 | import { useReduxDevToolsExtension } from '@react-navigation/devtools'; 2 | import { useDevToolsPluginClient } from 'expo/devtools'; 3 | import { useEffect, useRef } from 'react'; 4 | import { ReduxExtensionAdapter } from './ReduxExtensionAdapter'; 5 | export function useReactNavigationDevTools(ref) { 6 | const client = useDevToolsPluginClient('react-navigation'); 7 | const adapterRef = useRef(new ReduxExtensionAdapter()); 8 | // @ts-ignore: Override global 9 | globalThis.__REDUX_DEVTOOLS_EXTENSION__ = { 10 | connect: () => adapterRef.current, 11 | }; 12 | // @ts-expect-error 13 | useReduxDevToolsExtension(ref); 14 | useEffect(() => { 15 | adapterRef.current.setClient(client); 16 | const on = (event, listener) => { 17 | return client?.addMessageListener(event, async (params) => { 18 | try { 19 | const result = await listener(params); 20 | if (params.id) { 21 | client?.sendMessage(`ack:${event}`, { id: params.id, result }); 22 | } 23 | } 24 | catch { } 25 | }); 26 | }; 27 | const subscriptions = []; 28 | subscriptions.push(on('navigation.invoke', ({ method, args = [] }) => { 29 | switch (method) { 30 | case 'resetRoot': 31 | return adapterRef.current?.resetRoot(args[0]); 32 | default: 33 | // @ts-ignore: this might not exist 34 | return ref.current?.[method](...args); 35 | } 36 | })); 37 | subscriptions.push(on('linking.invoke', ({ method, args = [] }) => { 38 | const linking = ref.current 39 | ? // @ts-ignore: this might not exist 40 | global.REACT_NAVIGATION_DEVTOOLS?.get(ref.current)?.linking 41 | : null; 42 | switch (method) { 43 | case 'getStateFromPath': 44 | case 'getPathFromState': 45 | case 'getActionFromState': 46 | return linking?.[method](args[0], args[1]?.trim() 47 | ? // eslint-disable-next-line no-eval 48 | eval(`(function() { return ${args[1]}; }())`) 49 | : linking.config); 50 | default: 51 | return linking?.[method](...args); 52 | } 53 | })); 54 | return () => { 55 | for (const subscription of subscriptions) { 56 | subscription?.remove(); 57 | } 58 | }; 59 | }, [client]); 60 | } 61 | //# sourceMappingURL=useReactNavigationDevTools.js.map -------------------------------------------------------------------------------- /packages/react-navigation/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | const { defineConfig } = require('eslint/config'); 3 | const baseConfig = require('expo-module-scripts/eslint.config.base'); 4 | module.exports = defineConfig([baseConfig]); 5 | -------------------------------------------------------------------------------- /packages/react-navigation/expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["devtools"], 3 | "devtools": { 4 | "webpageRoot": "dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-navigation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/react-navigation", 3 | "version": "0.3.1", 4 | "description": "Expo DevTools Plugin for React Navigation", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "expo-module build", 10 | "clean": "expo-module clean", 11 | "lint": "expo-module lint", 12 | "test": "expo-module test", 13 | "prepare": "expo-module prepare && node ../../scripts/build-webui.js react-navigation", 14 | "prepublishOnly": "expo-module prepublishOnly", 15 | "expo-module": "expo-module" 16 | }, 17 | "homepage": "https://docs.expo.dev/versions/latest/sdk/image/", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/expo/dev-plugins.git", 21 | "directory": "packages/react-navigation" 22 | }, 23 | "keywords": [ 24 | "expo", 25 | "devtools", 26 | "react-navigation" 27 | ], 28 | "files": [ 29 | "build", 30 | "dist", 31 | "expo-module.config.json", 32 | "src" 33 | ], 34 | "author": "650 Industries, Inc.", 35 | "license": "MIT", 36 | "dependencies": { 37 | "@react-navigation/devtools": "^7.0.1", 38 | "nanoid": "^5.0.8" 39 | }, 40 | "devDependencies": { 41 | "@react-navigation/core": "^7.0.0", 42 | "expo": "53.0.7", 43 | "expo-module-scripts": "^4.1.7", 44 | "typescript": "~5.8.3" 45 | }, 46 | "peerDependencies": { 47 | "@react-navigation/core": "*", 48 | "expo": "^53.0.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-navigation/src/ReduxExtensionAdapter.ts: -------------------------------------------------------------------------------- 1 | import { type DevToolsPluginClient } from 'expo/devtools'; 2 | import { nanoid } from 'nanoid/non-secure'; 3 | 4 | type ReduxExtensionEventListener = (message: { type: string; [key: string]: any }) => void; 5 | 6 | /** 7 | * A stub Redux extension connector for `useReduxDevToolsExtension` 8 | */ 9 | export class ReduxExtensionAdapter { 10 | private client: DevToolsPluginClient | null = null; 11 | private reduxExtensionListener: ReduxExtensionEventListener | null = null; 12 | 13 | init(value: any): void { 14 | this.client?.sendMessage('init', { 15 | id: nanoid(), 16 | value, 17 | }); 18 | } 19 | 20 | send(action: any, state: any): void { 21 | this.client?.sendMessage('action', { 22 | id: nanoid(), 23 | action, 24 | state, 25 | }); 26 | } 27 | 28 | subscribe(reduxExtensionListener: ReduxExtensionEventListener) { 29 | this.reduxExtensionListener = reduxExtensionListener; 30 | } 31 | 32 | resetRoot(value: any): void { 33 | this.reduxExtensionListener?.({ type: 'DISPATCH', state: JSON.stringify(value) }); 34 | } 35 | 36 | setClient(client: DevToolsPluginClient | null) { 37 | this.client = client; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-navigation/src/index.ts: -------------------------------------------------------------------------------- 1 | export let useReactNavigationDevTools: typeof import('./useReactNavigationDevTools').useReactNavigationDevTools; 2 | 3 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 4 | if (process.env.NODE_ENV !== 'production') { 5 | useReactNavigationDevTools = require('./useReactNavigationDevTools').useReactNavigationDevTools; 6 | } else { 7 | useReactNavigationDevTools = () => {}; 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-navigation/src/useReactNavigationDevTools.ts: -------------------------------------------------------------------------------- 1 | import type { NavigationContainerRef } from '@react-navigation/core'; 2 | import { useReduxDevToolsExtension } from '@react-navigation/devtools'; 3 | import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 4 | import { useEffect, useRef } from 'react'; 5 | 6 | import { ReduxExtensionAdapter } from './ReduxExtensionAdapter'; 7 | 8 | export function useReactNavigationDevTools(ref: React.RefObject>) { 9 | const client = useDevToolsPluginClient('react-navigation'); 10 | const adapterRef = useRef(new ReduxExtensionAdapter()); 11 | // @ts-ignore: Override global 12 | globalThis.__REDUX_DEVTOOLS_EXTENSION__ = { 13 | connect: () => adapterRef.current, 14 | }; 15 | // @ts-expect-error 16 | useReduxDevToolsExtension(ref); 17 | 18 | useEffect(() => { 19 | adapterRef.current.setClient(client); 20 | 21 | const on = (event: string, listener: (params: any) => Promise) => { 22 | return client?.addMessageListener(event, async (params) => { 23 | try { 24 | const result = await listener(params); 25 | 26 | if (params.id) { 27 | client?.sendMessage(`ack:${event}`, { id: params.id, result }); 28 | } 29 | } catch {} 30 | }); 31 | }; 32 | 33 | const subscriptions: (EventSubscription | undefined)[] = []; 34 | subscriptions.push( 35 | on('navigation.invoke', ({ method, args = [] }) => { 36 | switch (method) { 37 | case 'resetRoot': 38 | return adapterRef.current?.resetRoot(args[0]); 39 | default: 40 | // @ts-ignore: this might not exist 41 | return ref.current?.[method](...args); 42 | } 43 | }) 44 | ); 45 | 46 | subscriptions.push( 47 | on('linking.invoke', ({ method, args = [] }) => { 48 | const linking: any = ref.current 49 | ? // @ts-ignore: this might not exist 50 | global.REACT_NAVIGATION_DEVTOOLS?.get(ref.current)?.linking 51 | : null; 52 | 53 | switch (method) { 54 | case 'getStateFromPath': 55 | case 'getPathFromState': 56 | case 'getActionFromState': 57 | return linking?.[method]( 58 | args[0], 59 | args[1]?.trim() 60 | ? // eslint-disable-next-line no-eval 61 | eval(`(function() { return ${args[1]}; }())`) 62 | : linking.config 63 | ); 64 | default: 65 | return linking?.[method](...args); 66 | } 67 | }) 68 | ); 69 | 70 | return () => { 71 | for (const subscription of subscriptions) { 72 | subscription?.remove(); 73 | } 74 | }; 75 | }, [client]); 76 | } 77 | -------------------------------------------------------------------------------- /packages/react-navigation/tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "web": { 4 | "bundler": "metro" 5 | }, 6 | "experiments": { 7 | "baseUrl": "/_expo/plugins/@dev-plugins/react-navigation" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/react-navigation-webui", 3 | "description": "The frontend webui for @dev-plugins/react-navigation", 4 | "private": true, 5 | "version": "0.3.1", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "expo start -w" 9 | }, 10 | "author": "650 Industries, Inc.", 11 | "license": "MIT", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "@ant-design/icons": "^5.5.1", 15 | "@babel/core": "^7.26.0", 16 | "@emotion/react": "^11.13.3", 17 | "@emotion/styled": "^11.13.0", 18 | "@react-navigation/core": "^7.0.0", 19 | "@types/react": "~19.0.10", 20 | "@types/react-virtualized-auto-sizer": "^1.0.4", 21 | "@types/react-window": "^1.8.8", 22 | "antd": "^5.22.0", 23 | "expo": "53.0.7", 24 | "nanoid": "^5.0.8", 25 | "react": "19.0.0", 26 | "react-dom": "19.0.0", 27 | "react-json-view": "^1.21.3", 28 | "react-native": "0.79.2", 29 | "react-native-web": "0.20.0", 30 | "react-virtualized-auto-sizer": "^1.0.24", 31 | "react-window": "^1.8.10", 32 | "typescript": "~5.8.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | import { Layout, Tabs, theme as antTheme, ThemeConfig } from 'antd'; 4 | import * as React from 'react'; 5 | 6 | import { LinkingTester } from './LinkingTester'; 7 | import { Logs } from './Logs'; 8 | import { theme } from './theme'; 9 | import { usePluginStore } from './usePluginStore'; 10 | 11 | declare module '@emotion/react' { 12 | export interface Theme extends ThemeConfig {} 13 | } 14 | 15 | const { TabPane } = Tabs; 16 | 17 | export default function App() { 18 | const store = usePluginStore(); 19 | const { token } = antTheme.useToken(); 20 | 21 | const [activeKey, setActiveKey] = React.useState('logs'); 22 | 23 | return ( 24 | 25 | 26 | 27 | Logs} key="logs"> 28 | 29 | 30 | Linking} key="linking"> 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | const Container = styled(Layout)({ 40 | padding: `0 ${theme.space.large}px`, 41 | }); 42 | 43 | const TabLabel = styled.span({ 44 | padding: `0 ${theme.space.large}px`, 45 | }); 46 | 47 | const TabsContent = styled(TabPane)({ 48 | height: 'calc(100vh - 80px)', 49 | }); 50 | 51 | // export * from './plugin'; 52 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/src/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { Layout } from 'antd'; 3 | import * as React from 'react'; 4 | import ReactJson from 'react-json-view'; 5 | 6 | import { Title4 } from './Typography'; 7 | import { theme } from './theme'; 8 | 9 | export function Sidebar({ 10 | action, 11 | state, 12 | stack, 13 | }: { 14 | action: object; 15 | state: object | undefined; 16 | stack?: string | undefined; 17 | }) { 18 | return ( 19 | 29 | {stack ? ( 30 | <> 31 | Stack 32 | 33 | {stack.split('\n').map((line, index) => { 34 | const match = line.match(/^(.+)@(.+):(\d+):(\d+)$/); 35 | 36 | if (match) { 37 | const [, methodName, file, lineNumber, column] = match; 38 | 39 | if (file.includes('/node_modules/@react-navigation')) { 40 | return null; 41 | } 42 | 43 | return ( 44 | // eslint-disable-next-line react/no-array-index-key 45 |
46 | {methodName.split('.').map((part, i, self) => { 47 | if (i === self.length - 1 && i !== 0) { 48 | return {part}; 49 | } 50 | 51 | if (self.length !== 1) { 52 | return ( 53 | <> 54 | {part} 55 | . 56 | 57 | ); 58 | } 59 | 60 | return part; 61 | })}{' '} 62 | ( 63 | {file.split('/').pop()} 64 | : 65 | {lineNumber}:{column} 66 | ) 67 |
68 | ); 69 | } 70 | 71 | return ( 72 | // eslint-disable-next-line react/no-array-index-key 73 |
{line}
74 | ); 75 | })} 76 |
77 | 78 | ) : null} 79 | Action 80 | 81 | State 82 | 83 |
84 | ); 85 | } 86 | 87 | const Code = styled.div({ 88 | fontSize: 11, 89 | fontFamily: theme.monospace.fontFamily, 90 | margin: '7.5px 0px', 91 | }); 92 | 93 | const StringToken = styled.span({ 94 | color: 'rgb(224, 76, 96)', 95 | }); 96 | 97 | const NumberToken = styled.span({ 98 | color: 'rgb(77, 187, 166)', 99 | }); 100 | 101 | const Method = styled.span({ 102 | color: 'rgb(123, 100, 192)', 103 | }); 104 | 105 | const Separator = styled.span({ 106 | color: '#555', 107 | }); 108 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/src/Typography.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | import { theme } from './theme'; 4 | 5 | export const Title4 = styled.h4({ 6 | fontWeight: 600, 7 | fontSize: theme.fontSize.default, 8 | lineHeight: 1.4, 9 | letterSpacing: -0.24, 10 | marginBottom: 0, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/src/theme.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copied from https://github.com/facebook/flipper/blob/50b06f2efdefd9f398d2ffcc0ffe1a883843226d/desktop/flipper-plugin/src/ui/theme.tsx 3 | * and replace the css variables with actual values from the light theme. 4 | */ 5 | 6 | /** 7 | * Copyright (c) Meta Platforms, Inc. and affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | * 12 | * @format 13 | */ 14 | 15 | import * as antColors from '@ant-design/colors'; 16 | 17 | // Exposes all the variables defined in themes/base.less: 18 | export const theme = { 19 | white: 'white', // use as counter color for primary 20 | black: 'black', 21 | // primaryColor: 'var(--flipper-primary-color)', 22 | // successColor: 'var(--flipper-success-color)', 23 | // errorColor: 'var(--flipper-error-color)', 24 | // warningColor: 'var(--flipper-warning-color)', 25 | // textColorPrimary: 'var(--flipper-text-color-primary)', 26 | textColorSecondary: '#666', 27 | textColorPlaceholder: '#8c8c8c', 28 | textColorActive: 'white', 29 | searchHighlightBackground: { 30 | yellow: antColors.yellow[3], 31 | red: antColors.red[3], 32 | green: antColors.green[3], 33 | blue: antColors.blue[3], 34 | } as const, 35 | selectionBackgroundColor: '#f2f2f2', 36 | disabledColor: '#bfbfbf', 37 | // backgroundDefault: 'var(--flipper-background-default)', 38 | backgroundWash: '#f2f2f2', 39 | buttonDefaultBackground: 'rgba(0, 0, 0, 0.1)', 40 | backgroundTransparentHover: 'rgba(0, 0, 0, 0.1)', 41 | dividerColor: '#ececec', 42 | borderRadius: '6px', 43 | containerBorderRadius: 8, 44 | inlinePaddingV: 6, // vertical padding on inline elements like buttons 45 | inlinePaddingH: 12, // horizontal ,,, 46 | space: { 47 | // from Space component in Ant 48 | tiny: 4, 49 | small: 8, 50 | medium: 12, 51 | large: 16, 52 | huge: 24, 53 | } as const, 54 | fontSize: { 55 | large: '16px', 56 | default: '14px', 57 | small: '12px', 58 | smaller: '10px', 59 | } as const, 60 | monospace: { 61 | fontFamily: 'SF Mono,Monaco,Andale Mono,monospace', 62 | fontSize: '12px', 63 | } as const, 64 | bold: 600, 65 | semanticColors: { 66 | attribute: antColors.orange[5], 67 | nullValue: antColors.grey.primary!, 68 | stringValue: antColors.orange[5], 69 | colorValue: antColors.cyan[5], 70 | booleanValue: antColors.magenta[5], 71 | numberValue: antColors.blue[5], 72 | // diffAddedBackground: 'var(--flipper-diff-added-background)', 73 | // diffRemovedBackground: 'var(--flipper-diff-removed-background)', 74 | }, 75 | } as const; 76 | 77 | export type Spacing = keyof (typeof theme)['space'] | number | undefined | boolean; 78 | 79 | export type PaddingProps = { 80 | padv?: Spacing; 81 | padh?: Spacing; 82 | pad?: Spacing; 83 | }; 84 | 85 | export function normalizePadding({ padv, padh, pad }: PaddingProps): string | undefined { 86 | if (padv === undefined && padh === undefined && pad === undefined) { 87 | return undefined; 88 | } 89 | return `${normalizeSpace(padv ?? pad ?? 0, theme.inlinePaddingV)}px ${normalizeSpace( 90 | padh ?? pad ?? 0, 91 | theme.inlinePaddingH 92 | )}px`; 93 | } 94 | 95 | export function normalizeSpace(spacing: Spacing, defaultSpace: number): number { 96 | return spacing === true 97 | ? defaultSpace 98 | : spacing === undefined || spacing === false 99 | ? 0 100 | : typeof spacing === 'string' 101 | ? theme.space[spacing] 102 | : spacing; 103 | } 104 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/src/types.ts: -------------------------------------------------------------------------------- 1 | export type NavigationRoute = { 2 | key: string; 3 | name: string; 4 | params?: object; 5 | state?: NavigationState; 6 | }; 7 | 8 | export type NavigationState = { 9 | key: string; 10 | index: number; 11 | routes: NavigationRoute[]; 12 | }; 13 | 14 | export type NavigationAction = { 15 | type: string; 16 | payload?: object; 17 | }; 18 | 19 | export type PartialRoute = { 20 | name: string; 21 | params?: object; 22 | state?: PartialState; 23 | }; 24 | 25 | export type PartialState = { 26 | routes: PartialRoute[]; 27 | }; 28 | 29 | export type Log = { 30 | id: string; 31 | action: NavigationAction; 32 | state: NavigationState | undefined; 33 | stack: string | undefined; 34 | }; 35 | 36 | export type StoreType = { 37 | logs: Log[]; 38 | index: number; 39 | navigation: (method: string, ...args: any[]) => Promise; 40 | linking: (method: string, ...args: any[]) => Promise; 41 | resetTo: (id: string) => Promise; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/src/usePluginStore.ts: -------------------------------------------------------------------------------- 1 | import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 2 | import { nanoid } from 'nanoid/non-secure'; 3 | import { useCallback, useEffect, useState } from 'react'; 4 | 5 | import type { Log } from './types'; 6 | 7 | interface Deferred { 8 | promise: Promise; 9 | resolve: (value: T | PromiseLike) => void; 10 | reject: (reason?: any) => void; 11 | } 12 | 13 | const promiseMap = new Map>(); 14 | 15 | export function usePluginStore() { 16 | const client = useDevToolsPluginClient('react-navigation'); 17 | const [logs, setLogs] = useState([]); 18 | const [index, setIndex] = useState(null); 19 | 20 | useEffect(() => { 21 | const subscriptions: (EventSubscription | undefined)[] = []; 22 | 23 | subscriptions.push( 24 | client?.addMessageListener('init', () => { 25 | setLogs([]); 26 | setIndex(null); 27 | }) 28 | ); 29 | 30 | subscriptions.push( 31 | client?.addMessageListener('action', (action) => { 32 | setLogs((value) => { 33 | const currentLogs = index !== null ? value.slice(0, index + 1) : value; 34 | return [...currentLogs, action]; 35 | }); 36 | setIndex(null); 37 | }) 38 | ); 39 | 40 | subscriptions.push( 41 | client?.addMessageListener('ack:linking.invoke', ({ id, result }) => { 42 | const deferred = promiseMap.get(id); 43 | if (deferred) { 44 | deferred.resolve(result); 45 | promiseMap.delete(id); 46 | } 47 | }) 48 | ); 49 | 50 | return () => { 51 | for (const subscription of subscriptions) { 52 | subscription?.remove(); 53 | } 54 | }; 55 | }, [client, index]); 56 | 57 | const navigation = useCallback( 58 | async (method: string, ...args: any[]) => { 59 | await client?.sendMessage('navigation.invoke', { method, args }); 60 | }, 61 | [client] 62 | ); 63 | 64 | const resetTo = useCallback( 65 | async (id: string) => { 66 | const indexValue = logs.findIndex((update) => update.id === id)!; 67 | const { state } = logs[indexValue]; 68 | setIndex(indexValue); 69 | await client?.sendMessage('navigation.invoke', { 70 | method: 'resetRoot', 71 | args: [state], 72 | }); 73 | }, 74 | [client, logs] 75 | ); 76 | 77 | const linking = useCallback( 78 | async (method: string, ...args: any[]) => { 79 | const id = nanoid(); 80 | const deferred = createDeferred(); 81 | promiseMap.set(id, deferred); 82 | await client?.sendMessage('linking.invoke', { method, args, id }); 83 | return deferred.promise; 84 | }, 85 | [client] 86 | ); 87 | 88 | return { 89 | logs, 90 | index: index ?? logs.length - 1, 91 | resetTo, 92 | navigation, 93 | linking, 94 | }; 95 | } 96 | 97 | function createDeferred(): Deferred { 98 | let resolve: (value: T | PromiseLike) => void; 99 | let reject: (reason?: any) => void; 100 | 101 | const promise = new Promise((res, rej) => { 102 | resolve = res; 103 | reject = rej; 104 | }); 105 | 106 | return { 107 | promise, 108 | resolve: resolve!, 109 | reject: reject!, 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /packages/react-navigation/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-query/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /packages/react-query/README.md: -------------------------------------------------------------------------------- 1 | # @dev-plugins/react-query 2 | 3 | A TanStack Query DevTool that can run in an Expo App 4 | 5 | # Installation 6 | 7 | ### Add the package to your project 8 | 9 | ``` 10 | npx expo install @dev-plugins/react-query 11 | ``` 12 | 13 | ### Integrate react-query with the DevTool hook 14 | 15 | ```jsx 16 | import { useReactQueryDevTools } from '@dev-plugins/react-query'; 17 | 18 | const queryClient = new QueryClient(...); 19 | 20 | export default function App() { 21 | useReactQueryDevTools(queryClient); 22 | 23 | return ( 24 | 25 | {/* ... */} 26 | 27 | ); 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /packages/react-query/babel.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/babel.config.base'); 3 | -------------------------------------------------------------------------------- /packages/react-query/build/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare let useReactQueryDevTools: typeof import('./useReactQueryDevTools').useReactQueryDevTools; 2 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /packages/react-query/build/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,IAAI,qBAAqB,EAAE,cAAc,yBAAyB,EAAE,qBAAqB,CAAC"} -------------------------------------------------------------------------------- /packages/react-query/build/index.js: -------------------------------------------------------------------------------- 1 | export let useReactQueryDevTools; 2 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 3 | if (process.env.NODE_ENV !== 'production') { 4 | useReactQueryDevTools = require('./useReactQueryDevTools').useReactQueryDevTools; 5 | } 6 | else { 7 | useReactQueryDevTools = () => { }; 8 | } 9 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /packages/react-query/build/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,IAAI,qBAAqF,CAAC;AAEjG,wEAAwE;AACxE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,qBAAqB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC,qBAAqB,CAAC;AACnF,CAAC;KAAM,CAAC;IACN,qBAAqB,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AACnC,CAAC","sourcesContent":["export let useReactQueryDevTools: typeof import('./useReactQueryDevTools').useReactQueryDevTools;\n\n// @ts-ignore process.env.NODE_ENV is defined by metro transform plugins\nif (process.env.NODE_ENV !== 'production') {\n useReactQueryDevTools = require('./useReactQueryDevTools').useReactQueryDevTools;\n} else {\n useReactQueryDevTools = () => {};\n}\n"]} -------------------------------------------------------------------------------- /packages/react-query/build/useReactQueryDevTools.d.ts: -------------------------------------------------------------------------------- 1 | import type { QueryClient } from '@tanstack/react-query'; 2 | export declare function useReactQueryDevTools(queryClient: QueryClient): void; 3 | //# sourceMappingURL=useReactQueryDevTools.d.ts.map -------------------------------------------------------------------------------- /packages/react-query/build/useReactQueryDevTools.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"useReactQueryDevTools.d.ts","sourceRoot":"","sources":["../src/useReactQueryDevTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgC,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAOvF,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,QAqE7D"} -------------------------------------------------------------------------------- /packages/react-query/build/useReactQueryDevTools.js: -------------------------------------------------------------------------------- 1 | import { useDevToolsPluginClient } from 'expo/devtools'; 2 | import { stringify } from 'flatted'; 3 | import { useEffect } from 'react'; 4 | const bigintReplacer = (_, v) => (typeof v === 'bigint' ? v.toString() : v); 5 | export function useReactQueryDevTools(queryClient) { 6 | const client = useDevToolsPluginClient('react-query'); 7 | const queryCache = queryClient.getQueryCache(); 8 | let unsubscribe; 9 | function getQueries() { 10 | return queryCache.getAll(); 11 | } 12 | function getQueryByHash(queryHash) { 13 | return getQueries().find((query) => query.queryHash === queryHash); 14 | } 15 | function getSerializedQueries() { 16 | const queries = getQueries().map((query) => serializeQuery(query)); 17 | const serializedQueries = { 18 | queries: stringify(queries, bigintReplacer), 19 | }; 20 | return serializedQueries; 21 | } 22 | useEffect(() => { 23 | const subscriptions = []; 24 | subscriptions.push(client?.addMessageListener('queryRefetch', ({ queryHash }) => { 25 | getQueryByHash(queryHash)?.fetch(); 26 | })); 27 | subscriptions.push(client?.addMessageListener('queryRemove', ({ queryHash }) => { 28 | const query = getQueryByHash(queryHash); 29 | if (query) { 30 | queryClient.removeQueries({ queryKey: query.queryKey, exact: true }); 31 | } 32 | })); 33 | // send initial queries 34 | client?.sendMessage('queries', getSerializedQueries()); 35 | /** 36 | * handles QueryCacheNotifyEvent 37 | * @param event - QueryCacheNotifyEvent, but RQ doesn't have it exported 38 | */ 39 | const handleCacheEvent = (event) => { 40 | const { query } = event; 41 | client?.sendMessage('queryCacheEvent', { 42 | cacheEvent: stringify({ ...event, query: serializeQuery(query) }, bigintReplacer), 43 | }); 44 | }; 45 | // Subscribe to QueryCacheNotifyEvent and send updates only 46 | unsubscribe = queryCache.subscribe(handleCacheEvent); 47 | return () => { 48 | if (unsubscribe) { 49 | unsubscribe(); 50 | unsubscribe = undefined; 51 | } 52 | for (const subscription of subscriptions) { 53 | subscription?.remove(); 54 | } 55 | }; 56 | }, [client]); 57 | } 58 | function serializeQuery(query) { 59 | return { 60 | ...query, 61 | _ext_isActive: query.isActive(), 62 | _ext_isStale: query.isStale(), 63 | _ext_observersCount: query.getObserversCount(), 64 | }; 65 | } 66 | //# sourceMappingURL=useReactQueryDevTools.js.map -------------------------------------------------------------------------------- /packages/react-query/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | const { defineConfig } = require('eslint/config'); 3 | const baseConfig = require('expo-module-scripts/eslint.config.base'); 4 | module.exports = defineConfig([baseConfig]); 5 | -------------------------------------------------------------------------------- /packages/react-query/expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["devtools"], 3 | "devtools": { 4 | "webpageRoot": "dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/react-query", 3 | "version": "0.3.1", 4 | "description": "Expo DevTools Plugin for TanStack Query", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "expo-module build", 10 | "clean": "expo-module clean", 11 | "lint": "expo-module lint", 12 | "test": "expo-module test", 13 | "prepare": "expo-module prepare && node ../../scripts/build-webui.js react-query", 14 | "prepublishOnly": "expo-module prepublishOnly", 15 | "expo-module": "expo-module" 16 | }, 17 | "homepage": "https://docs.expo.dev/versions/latest/sdk/image/", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/expo/dev-plugins.git", 21 | "directory": "packages/react-query" 22 | }, 23 | "keywords": [ 24 | "expo", 25 | "devtools", 26 | "tanstack", 27 | "react-query" 28 | ], 29 | "files": [ 30 | "build", 31 | "dist", 32 | "expo-module.config.json", 33 | "src" 34 | ], 35 | "author": "650 Industries, Inc.", 36 | "license": "MIT", 37 | "dependencies": { 38 | "flatted": "^3.3.1" 39 | }, 40 | "devDependencies": { 41 | "@tanstack/react-query": "^5.59.20", 42 | "expo": "53.0.7", 43 | "expo-module-scripts": "^4.1.7", 44 | "typescript": "~5.8.3" 45 | }, 46 | "peerDependencies": { 47 | "@tanstack/react-query": "*", 48 | "expo": "^53.0.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-query/src/index.ts: -------------------------------------------------------------------------------- 1 | export let useReactQueryDevTools: typeof import('./useReactQueryDevTools').useReactQueryDevTools; 2 | 3 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 4 | if (process.env.NODE_ENV !== 'production') { 5 | useReactQueryDevTools = require('./useReactQueryDevTools').useReactQueryDevTools; 6 | } else { 7 | useReactQueryDevTools = () => {}; 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-query/src/useReactQueryDevTools.ts: -------------------------------------------------------------------------------- 1 | import type { Query, QueryCacheNotifyEvent, QueryClient } from '@tanstack/react-query'; 2 | import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 3 | import { stringify } from 'flatted'; 4 | import { useEffect } from 'react'; 5 | 6 | const bigintReplacer = (_: any, v: any) => (typeof v === 'bigint' ? v.toString() : v); 7 | 8 | export function useReactQueryDevTools(queryClient: QueryClient) { 9 | const client = useDevToolsPluginClient('react-query'); 10 | const queryCache = queryClient.getQueryCache(); 11 | 12 | let unsubscribe: (() => void) | undefined; 13 | 14 | function getQueries() { 15 | return queryCache.getAll(); 16 | } 17 | 18 | function getQueryByHash(queryHash: string): Query | undefined { 19 | return getQueries().find((query) => query.queryHash === queryHash); 20 | } 21 | 22 | function getSerializedQueries() { 23 | const queries = getQueries().map((query) => serializeQuery(query)); 24 | 25 | const serializedQueries = { 26 | queries: stringify(queries, bigintReplacer), 27 | }; 28 | 29 | return serializedQueries; 30 | } 31 | 32 | useEffect(() => { 33 | const subscriptions: (EventSubscription | undefined)[] = []; 34 | 35 | subscriptions.push( 36 | client?.addMessageListener('queryRefetch', ({ queryHash }) => { 37 | getQueryByHash(queryHash)?.fetch(); 38 | }) 39 | ); 40 | 41 | subscriptions.push( 42 | client?.addMessageListener('queryRemove', ({ queryHash }) => { 43 | const query = getQueryByHash(queryHash); 44 | if (query) { 45 | queryClient.removeQueries({ queryKey: query.queryKey, exact: true }); 46 | } 47 | }) 48 | ); 49 | 50 | // send initial queries 51 | client?.sendMessage('queries', getSerializedQueries()); 52 | 53 | /** 54 | * handles QueryCacheNotifyEvent 55 | * @param event - QueryCacheNotifyEvent, but RQ doesn't have it exported 56 | */ 57 | const handleCacheEvent = (event: QueryCacheNotifyEvent) => { 58 | const { query } = event; 59 | client?.sendMessage('queryCacheEvent', { 60 | cacheEvent: stringify({ ...event, query: serializeQuery(query) }, bigintReplacer), 61 | }); 62 | }; 63 | 64 | // Subscribe to QueryCacheNotifyEvent and send updates only 65 | unsubscribe = queryCache.subscribe(handleCacheEvent); 66 | 67 | return () => { 68 | if (unsubscribe) { 69 | unsubscribe(); 70 | unsubscribe = undefined; 71 | } 72 | for (const subscription of subscriptions) { 73 | subscription?.remove(); 74 | } 75 | }; 76 | }, [client]); 77 | } 78 | 79 | function serializeQuery(query: Query) { 80 | return { 81 | ...query, 82 | _ext_isActive: query.isActive(), 83 | _ext_isStale: query.isStale(), 84 | _ext_observersCount: query.getObserversCount(), 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /packages/react-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-query/webui/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "web": { 4 | "bundler": "metro" 5 | }, 6 | "experiments": { 7 | "baseUrl": "/_expo/plugins/@dev-plugins/react-query" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-query/webui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/react-query/webui/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /packages/react-query/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/react-query-webui", 3 | "description": "The frontend webui for @dev-plugins/react-query", 4 | "private": true, 5 | "version": "0.3.1", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "expo start -w" 9 | }, 10 | "author": "650 Industries, Inc.", 11 | "license": "MIT", 12 | "dependencies": { 13 | "flatted": "^3.3.1" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.26.0", 17 | "@emotion/react": "^11.13.3", 18 | "@emotion/styled": "^11.13.0", 19 | "@tanstack/react-query": "^5.59.20", 20 | "@types/lodash": "^4.17.13", 21 | "@types/react": "~19.0.10", 22 | "antd": "^5.22.0", 23 | "expo": "53.0.7", 24 | "lodash": "^4.17.21", 25 | "nanoid": "^5.0.8", 26 | "react": "19.0.0", 27 | "react-dom": "19.0.0", 28 | "react-json-view": "^1.21.3", 29 | "react-native": "0.79.2", 30 | "react-native-web": "0.20.0", 31 | "typescript": "~5.8.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/react-query/webui/src/components/DataViewer.tsx: -------------------------------------------------------------------------------- 1 | import ReactJson from 'react-json-view'; 2 | 3 | export default function DataViewer({ src }: { src: unknown }) { 4 | if (src === null || src === undefined) { 5 | return {src}; 6 | } 7 | if (typeof src === 'object') { 8 | return ; 9 | } 10 | return {src.toString()}; 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-query/webui/src/components/QuerySidebar.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { Button, Collapse, type CollapseProps, Layout, Space } from 'antd'; 3 | import ReactJson from 'react-json-view'; 4 | 5 | import type { ExtendedQuery } from '../types'; 6 | import DataViewer from './DataViewer'; 7 | 8 | const ContainerWithPaddings = styled(Layout)({ 9 | padding: '10px 5px', 10 | }); 11 | 12 | interface Props { 13 | query: ExtendedQuery | null; 14 | onQueryRefetch: (query: ExtendedQuery) => void; 15 | onQueryRemove: (query: ExtendedQuery) => void; 16 | } 17 | 18 | export default function QuerySidebar({ query, onQueryRefetch, onQueryRemove }: Props) { 19 | const panels: CollapseProps['items'] = [ 20 | { 21 | key: '1', 22 | label: 'Actions', 23 | children: ( 24 | 25 | 35 | 45 | 46 | ), 47 | }, 48 | { 49 | key: '2', 50 | label: 'Data Explorer', 51 | children: ( 52 | 53 | 54 | 55 | ), 56 | }, 57 | { 58 | key: '3', 59 | label: 'Query Explorer', 60 | children: ( 61 | 62 | 63 | 64 | ), 65 | }, 66 | ]; 67 | 68 | return ( 69 | 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /packages/react-query/webui/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Query, QueryStatus } from '@tanstack/react-query'; 2 | 3 | export type SerializedQuery = Query & { 4 | _ext_isActive: boolean; 5 | _ext_isStale: boolean; 6 | _ext_observersCount: number; 7 | }; 8 | 9 | export type ExtendedQuery = SerializedQuery & { 10 | key: string; 11 | status: QueryStatus; 12 | dataUpdateCount: number; 13 | observersCount: number; 14 | isQueryActive: boolean; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/react-query/webui/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import padStart from 'lodash/padStart'; 2 | import { nanoid } from 'nanoid'; 3 | 4 | import type { SerializedQuery } from '../types'; 5 | 6 | export function formatTimestamp(timestamp: number): string { 7 | if (timestamp === 0) { 8 | return '-'; 9 | } 10 | 11 | const date = new Date(timestamp); 12 | 13 | return `${padStart(date.getHours().toString(), 2, '0')}:${padStart( 14 | date.getMinutes().toString(), 15 | 2, 16 | '0' 17 | )}:${padStart(date.getSeconds().toString(), 2, '0')}.${padStart( 18 | date.getMilliseconds().toString(), 19 | 3, 20 | '0' 21 | )}`; 22 | } 23 | 24 | export function getObserversCounter(query: SerializedQuery): number { 25 | return query._ext_observersCount; 26 | } 27 | 28 | export function isQueryActive(query: SerializedQuery): boolean { 29 | return query._ext_isActive; 30 | } 31 | 32 | function isStale(query: SerializedQuery): boolean { 33 | const hasStaleObserver = query._ext_isStale; 34 | const hasInvalidState = query.state.isInvalidated || !query.state.dataUpdatedAt; 35 | 36 | return hasStaleObserver || hasInvalidState; 37 | } 38 | 39 | function isInactive(query: SerializedQuery): boolean { 40 | return getObserversCounter(query) === 0; 41 | } 42 | 43 | export function getQueryStatusLabel(query: SerializedQuery): string { 44 | return query.state.fetchStatus === 'fetching' 45 | ? 'fetching' 46 | : isInactive(query) 47 | ? 'inactive' 48 | : query.state.fetchStatus === 'paused' 49 | ? 'paused' 50 | : isStale(query) 51 | ? 'stale' 52 | : 'fresh'; 53 | } 54 | -------------------------------------------------------------------------------- /packages/react-query/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/tinybase/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /packages/tinybase/README.md: -------------------------------------------------------------------------------- 1 | # @dev-plugins/tinybase 2 | 3 | A TinyBase DevTool that can run in an Expo App 4 | 5 | # Installation 6 | 7 | ### Add the package to your project 8 | 9 | ``` 10 | npx expo install @dev-plugins/tinybase 11 | ``` 12 | 13 | ### Integrate TinyBase with the DevTool hook 14 | 15 | ```jsx 16 | import { useTinyBaseDevTools } from '@dev-plugins/tinybase'; 17 | 18 | const store = createStore(); 19 | 20 | export default function App() { 21 | useTinyBaseDevTools(store); 22 | 23 | return {/* ... */}; 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/tinybase/babel.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/babel.config.base'); 3 | -------------------------------------------------------------------------------- /packages/tinybase/build/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare let useTinyBaseDevTools: typeof import('./useTinyBaseDevTools').useTinyBaseDevTools; 2 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /packages/tinybase/build/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,IAAI,mBAAmB,EAAE,cAAc,uBAAuB,EAAE,mBAAmB,CAAC"} -------------------------------------------------------------------------------- /packages/tinybase/build/index.js: -------------------------------------------------------------------------------- 1 | export let useTinyBaseDevTools; 2 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 3 | if (process.env.NODE_ENV !== 'production') { 4 | useTinyBaseDevTools = require('./useTinyBaseDevTools').useTinyBaseDevTools; 5 | } 6 | else { 7 | useTinyBaseDevTools = () => { }; 8 | } 9 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /packages/tinybase/build/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,IAAI,mBAA+E,CAAC;AAE3F,wEAAwE;AACxE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC,mBAAmB,CAAC;AAC7E,CAAC;KAAM,CAAC;IACN,mBAAmB,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AACjC,CAAC","sourcesContent":["export let useTinyBaseDevTools: typeof import('./useTinyBaseDevTools').useTinyBaseDevTools;\n\n// @ts-ignore process.env.NODE_ENV is defined by metro transform plugins\nif (process.env.NODE_ENV !== 'production') {\n useTinyBaseDevTools = require('./useTinyBaseDevTools').useTinyBaseDevTools;\n} else {\n useTinyBaseDevTools = () => {};\n}\n"]} -------------------------------------------------------------------------------- /packages/tinybase/build/useTinyBaseDevTools.d.ts: -------------------------------------------------------------------------------- 1 | import type { Store } from 'tinybase'; 2 | export declare function useTinyBaseDevTools(store: Store): void; 3 | //# sourceMappingURL=useTinyBaseDevTools.d.ts.map -------------------------------------------------------------------------------- /packages/tinybase/build/useTinyBaseDevTools.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"useTinyBaseDevTools.d.ts","sourceRoot":"","sources":["../src/useTinyBaseDevTools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEtC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,QA0B/C"} -------------------------------------------------------------------------------- /packages/tinybase/build/useTinyBaseDevTools.js: -------------------------------------------------------------------------------- 1 | import { useDevToolsPluginClient } from 'expo/devtools'; 2 | import { useEffect } from 'react'; 3 | export function useTinyBaseDevTools(store) { 4 | const client = useDevToolsPluginClient('tinybase'); 5 | useEffect(() => { 6 | const subscriptions = []; 7 | /* Sync the full store on init */ 8 | client?.sendMessage('@tinybase-inspector/init', store.getJson()); 9 | subscriptions.push(client?.addMessageListener('@tinybase-inspector/edit', (data) => { 10 | store.setJson(data); 11 | })); 12 | const listenerId = store.addDidFinishTransactionListener(() => { 13 | client?.sendMessage('@tinybase-inspector/update', store.getJson()); 14 | }); 15 | return () => { 16 | store.delListener(listenerId); 17 | for (const subscription of subscriptions) { 18 | subscription?.remove(); 19 | } 20 | }; 21 | }, [client, store]); 22 | } 23 | //# sourceMappingURL=useTinyBaseDevTools.js.map -------------------------------------------------------------------------------- /packages/tinybase/build/useTinyBaseDevTools.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"useTinyBaseDevTools.js","sourceRoot":"","sources":["../src/useTinyBaseDevTools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAA0B,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGlC,MAAM,UAAU,mBAAmB,CAAC,KAAY;IAC9C,MAAM,MAAM,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;IAEnD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,aAAa,GAAsC,EAAE,CAAC;QAE5D,iCAAiC;QACjC,MAAM,EAAE,WAAW,CAAC,0BAA0B,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAEjE,aAAa,CAAC,IAAI,CAChB,MAAM,EAAE,kBAAkB,CAAC,0BAA0B,EAAE,CAAC,IAAI,EAAE,EAAE;YAC9D,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC,CAAC,CACH,CAAC;QAEF,MAAM,UAAU,GAAG,KAAK,CAAC,+BAA+B,CAAC,GAAG,EAAE;YAC5D,MAAM,EAAE,WAAW,CAAC,4BAA4B,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACrE,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,KAAK,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;YAC9B,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;gBACzC,YAAY,EAAE,MAAM,EAAE,CAAC;YACzB,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AACtB,CAAC","sourcesContent":["import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools';\nimport { useEffect } from 'react';\nimport type { Store } from 'tinybase';\n\nexport function useTinyBaseDevTools(store: Store) {\n const client = useDevToolsPluginClient('tinybase');\n\n useEffect(() => {\n const subscriptions: (EventSubscription | undefined)[] = [];\n\n /* Sync the full store on init */\n client?.sendMessage('@tinybase-inspector/init', store.getJson());\n\n subscriptions.push(\n client?.addMessageListener('@tinybase-inspector/edit', (data) => {\n store.setJson(data);\n })\n );\n\n const listenerId = store.addDidFinishTransactionListener(() => {\n client?.sendMessage('@tinybase-inspector/update', store.getJson());\n });\n\n return () => {\n store.delListener(listenerId);\n for (const subscription of subscriptions) {\n subscription?.remove();\n }\n };\n }, [client, store]);\n}\n"]} -------------------------------------------------------------------------------- /packages/tinybase/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | const { defineConfig } = require('eslint/config'); 3 | const baseConfig = require('expo-module-scripts/eslint.config.base'); 4 | module.exports = defineConfig([baseConfig]); 5 | -------------------------------------------------------------------------------- /packages/tinybase/expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["devtools"], 3 | "devtools": { 4 | "webpageRoot": "dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/tinybase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/tinybase", 3 | "version": "0.3.1", 4 | "description": "Expo DevTools Plugin for TinyBase", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "expo-module build", 10 | "clean": "expo-module clean", 11 | "lint": "expo-module lint", 12 | "test": "expo-module test", 13 | "prepare": "expo-module prepare && node ../../scripts/build-webui.js tinybase", 14 | "prepublishOnly": "expo-module prepublishOnly", 15 | "expo-module": "expo-module" 16 | }, 17 | "homepage": "https://docs.expo.dev/versions/latest/sdk/image/", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/expo/dev-plugins.git", 21 | "directory": "packages/tinybase" 22 | }, 23 | "keywords": [ 24 | "expo", 25 | "devtools", 26 | "tinybase" 27 | ], 28 | "files": [ 29 | "build", 30 | "dist", 31 | "expo-module.config.json", 32 | "src" 33 | ], 34 | "author": "650 Industries, Inc.", 35 | "license": "MIT", 36 | "dependencies": {}, 37 | "devDependencies": { 38 | "expo": "53.0.7", 39 | "expo-module-scripts": "^4.1.7", 40 | "tinybase": "^5.3.8", 41 | "typescript": "~5.8.3" 42 | }, 43 | "peerDependencies": { 44 | "expo": "^53.0.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/tinybase/src/index.ts: -------------------------------------------------------------------------------- 1 | export let useTinyBaseDevTools: typeof import('./useTinyBaseDevTools').useTinyBaseDevTools; 2 | 3 | // @ts-ignore process.env.NODE_ENV is defined by metro transform plugins 4 | if (process.env.NODE_ENV !== 'production') { 5 | useTinyBaseDevTools = require('./useTinyBaseDevTools').useTinyBaseDevTools; 6 | } else { 7 | useTinyBaseDevTools = () => {}; 8 | } 9 | -------------------------------------------------------------------------------- /packages/tinybase/src/useTinyBaseDevTools.ts: -------------------------------------------------------------------------------- 1 | import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 2 | import { useEffect } from 'react'; 3 | import type { Store } from 'tinybase'; 4 | 5 | export function useTinyBaseDevTools(store: Store) { 6 | const client = useDevToolsPluginClient('tinybase'); 7 | 8 | useEffect(() => { 9 | const subscriptions: (EventSubscription | undefined)[] = []; 10 | 11 | /* Sync the full store on init */ 12 | client?.sendMessage('@tinybase-inspector/init', store.getJson()); 13 | 14 | subscriptions.push( 15 | client?.addMessageListener('@tinybase-inspector/edit', (data) => { 16 | store.setJson(data); 17 | }) 18 | ); 19 | 20 | const listenerId = store.addDidFinishTransactionListener(() => { 21 | client?.sendMessage('@tinybase-inspector/update', store.getJson()); 22 | }); 23 | 24 | return () => { 25 | store.delListener(listenerId); 26 | for (const subscription of subscriptions) { 27 | subscription?.remove(); 28 | } 29 | }; 30 | }, [client, store]); 31 | } 32 | -------------------------------------------------------------------------------- /packages/tinybase/tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/tinybase/webui/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "web": { 4 | "bundler": "metro" 5 | }, 6 | "experiments": { 7 | "baseUrl": "/_expo/plugins/@dev-plugins/tinybase" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/tinybase/webui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/tinybase/webui/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /packages/tinybase/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/tinybase-webui", 3 | "description": "The frontend webui for @dev-plugins/tinybase", 4 | "private": true, 5 | "version": "0.3.1", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "expo start -w" 9 | }, 10 | "author": "650 Industries, Inc.", 11 | "license": "MIT", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "@babel/core": "^7.26.0", 15 | "@types/react": "~19.0.10", 16 | "expo": "53.0.7", 17 | "react": "19.0.0", 18 | "react-dom": "19.0.0", 19 | "react-native": "0.79.2", 20 | "react-native-web": "0.20.0", 21 | "tinybase": "^5.3.8", 22 | "typescript": "~5.8.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/tinybase/webui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useDevToolsPluginClient, type EventSubscription } from 'expo/devtools'; 2 | import { useEffect } from 'react'; 3 | import { createStore } from 'tinybase'; 4 | import { Provider } from 'tinybase/ui-react'; 5 | import { Inspector } from 'tinybase/ui-react-inspector'; 6 | 7 | const store = createStore(); 8 | 9 | let __ignoreStoreUpdates = false; 10 | 11 | export default function App() { 12 | const client = useDevToolsPluginClient('tinybase'); 13 | 14 | useEffect(() => { 15 | const subscriptions: (EventSubscription | undefined)[] = []; 16 | 17 | subscriptions.push( 18 | client?.addMessageListener('@tinybase-inspector/init', (data) => { 19 | __ignoreStoreUpdates = true; 20 | store.setJson(data); 21 | __ignoreStoreUpdates = false; 22 | }) 23 | ); 24 | 25 | subscriptions.push( 26 | client?.addMessageListener('@tinybase-inspector/update', (data) => { 27 | __ignoreStoreUpdates = true; 28 | store.setJson(data); 29 | __ignoreStoreUpdates = false; 30 | }) 31 | ); 32 | 33 | const listenerId = store.addDidFinishTransactionListener(() => { 34 | if (__ignoreStoreUpdates) { 35 | return; 36 | } 37 | client?.sendMessage('@tinybase-inspector/edit', store.getJson()); 38 | }); 39 | 40 | return () => { 41 | store.delListener(listenerId); 42 | for (const subscription of subscriptions) { 43 | subscription?.remove(); 44 | } 45 | }; 46 | }, [client, store]); 47 | 48 | if (client == null) { 49 | return null; 50 | } 51 | 52 | return ( 53 | 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/tinybase/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/vanilla-log-viewer/README.md: -------------------------------------------------------------------------------- 1 | # @dev-plugins/vanilla-log-viewer 2 | 3 | An example of DevTools plugin using vanilla JavaScript to show console logs 4 | 5 | # Installation 6 | 7 | ### Add the package to your project 8 | 9 | ``` 10 | npx expo install @dev-plugins/vanilla-log-viewer 11 | ``` 12 | 13 | ### Integrate the plugin with your app 14 | 15 | ```jsx 16 | import { useVanillaLogViewer } from '@dev-plugins/vanilla-log-viewer'; 17 | 18 | export default function App() { 19 | useVanillaLogViewer(); 20 | 21 | return {/* ... */}; 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/vanilla-log-viewer/expo-module.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": ["devtools"], 3 | "devtools": { 4 | "webpageRoot": "webui-dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/vanilla-log-viewer/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare function useVanillaLogViewer(): void; 2 | -------------------------------------------------------------------------------- /packages/vanilla-log-viewer/index.js: -------------------------------------------------------------------------------- 1 | export let useVanillaLogViewer; 2 | 3 | if (process.env.NODE_ENV === 'development') { 4 | const { useDevToolsPluginClient } = require('expo/devtools'); 5 | const { useEffect } = require('react'); 6 | 7 | const originalConsoleLog = console.log; 8 | const originalConsoleWarn = console.warn; 9 | const originalConsoleError = console.error; 10 | 11 | useVanillaLogViewer = () => { 12 | const client = useDevToolsPluginClient('vanilla-log-viewer'); 13 | 14 | useEffect(() => { 15 | async function setup() { 16 | console.log = function (...args) { 17 | const payload = args.length === 1 ? args[0] : JSON.stringify(args); 18 | client?.sendMessage('log', payload); 19 | originalConsoleLog.apply(console, arguments); 20 | }; 21 | console.warn = function (...args) { 22 | const payload = args.length === 1 ? args[0] : JSON.stringify(args); 23 | client?.sendMessage('warn', payload); 24 | originalConsoleWarn.apply(console, arguments); 25 | }; 26 | console.error = function (...args) { 27 | const payload = args.length === 1 ? args[0] : JSON.stringify(args); 28 | client?.sendMessage('error', payload); 29 | originalConsoleError.apply(console, arguments); 30 | }; 31 | } 32 | 33 | async function teardown() { 34 | console.log = originalConsoleLog; 35 | console.warn = originalConsoleWarn; 36 | console.error = originalConsoleError; 37 | } 38 | 39 | setup(); 40 | return () => { 41 | teardown(); 42 | }; 43 | }, [client]); 44 | }; 45 | } else { 46 | useVanillaLogViewer = () => { 47 | // noop 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /packages/vanilla-log-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-plugins/vanilla-log-viewer", 3 | "version": "0.3.1", 4 | "description": "An example of DevTools plugin using vanilla JavaScript to show console logs", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepare": "esbuild --bundle webui-dist/script.js --outfile=webui-dist/script-bundle.js --minify" 10 | }, 11 | "keywords": [ 12 | "expo", 13 | "devtools", 14 | "console", 15 | "logviewer" 16 | ], 17 | "files": [ 18 | "webui-dist", 19 | "expo-module.config.json" 20 | ], 21 | "license": "MIT", 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "esbuild": "^0.24.0", 25 | "expo": "53.0.7" 26 | }, 27 | "peerDependencies": { 28 | "expo": "^53.0.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/vanilla-log-viewer/webui-dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Console Log Display 9 | 10 | 11 | 12 | 13 |

Console Log Viewer

14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/vanilla-log-viewer/webui-dist/script.js: -------------------------------------------------------------------------------- 1 | import { getDevToolsPluginClientAsync } from 'expo/devtools'; 2 | 3 | (async function () { 4 | var logContainer = document.getElementById('root'); 5 | 6 | function createLogItem(message, type) { 7 | if (typeof message == 'object') { 8 | message = JSON.stringify(message, null, 2); 9 | } 10 | var logItem = document.createElement('div'); 11 | logItem.className = 'log-item ' + type; 12 | logItem.textContent = message; 13 | logContainer.appendChild(logItem); 14 | } 15 | 16 | const client = await getDevToolsPluginClientAsync('vanilla-log-viewer'); 17 | client.addMessageListener('log', (message) => { 18 | createLogItem(message, 'log'); 19 | }); 20 | client.addMessageListener('warn', (message) => { 21 | createLogItem(message, 'warn'); 22 | }); 23 | client.addMessageListener('error', (message) => { 24 | createLogItem(message, 'error'); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /packages/vanilla-log-viewer/webui-dist/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Arial', sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | background-color: #f4f4f4; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | height: 100vh; 11 | } 12 | 13 | h1 { 14 | color: #333; 15 | } 16 | 17 | #root { 18 | border: 2px solid #333; 19 | border-radius: 8px; 20 | padding: 2vh; 21 | width: 80vw; 22 | max-width: 800px; 23 | margin-top: 2vh; 24 | background-color: #ffffff; 25 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 26 | overflow-y: auto; 27 | max-height: 80vh; 28 | } 29 | 30 | .log-item { 31 | border-left: 5px solid; 32 | padding: 5px 10px; 33 | margin: 5px 0; 34 | font-family: monospace; 35 | white-space: pre-wrap; 36 | } 37 | 38 | .log-item.log { 39 | background-color: #e7f4e4; 40 | border-color: #2e7d32; 41 | } 42 | 43 | .log-item.warn { 44 | background-color: #fff3e0; 45 | border-color: #ffa000; 46 | } 47 | 48 | .log-item.error { 49 | background-color: #ffebee; 50 | border-color: #d32f2f; 51 | } 52 | -------------------------------------------------------------------------------- /scripts/build-webui.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const spawnAsync = require('@expo/spawn-async'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | const ROOT = path.resolve(__dirname, '..'); 8 | 9 | async function runAsync() { 10 | const args = process.argv.slice(2); 11 | if (args.length === 0) { 12 | console.log(`Usage: ${path.basename(process.argv[1])} package [package2...]`); 13 | process.exit(1); 14 | } 15 | 16 | const packageNames = args; 17 | for (const packageName of packageNames) { 18 | await buildAsync(packageName); 19 | } 20 | } 21 | 22 | async function buildAsync(packageName) { 23 | console.log(`⚙️ Building web assets for ${packageName}`); 24 | const packageRoot = path.join(ROOT, 'packages', packageName); 25 | await Promise.all([ 26 | fs.promises.rm(path.join(packageRoot, 'dist'), { recursive: true, force: true }), 27 | fs.promises.rm(path.join(packageRoot, 'webui', 'dist'), { recursive: true, force: true }), 28 | ]); 29 | await spawnAsync('npx', ['expo', 'export', '-p', 'web', '--output-dir', 'dist'], { 30 | cwd: path.join(packageRoot, 'webui'), 31 | }); 32 | await fs.promises.rename(path.join(packageRoot, 'webui', 'dist'), path.join(packageRoot, 'dist')); 33 | } 34 | 35 | (async () => { 36 | try { 37 | await runAsync(); 38 | } catch (e) { 39 | console.error('Uncaught Error', e); 40 | process.exit(1); 41 | } 42 | })(); 43 | -------------------------------------------------------------------------------- /scripts/prepare-packages.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const spawnAsync = require('@expo/spawn-async'); 4 | const fs = require('fs/promises'); 5 | const path = require('path'); 6 | 7 | const ROOT = path.resolve(__dirname, '..'); 8 | 9 | async function runAsync() { 10 | const packageNames = (await fs.readdir(path.join(ROOT, 'packages'))).filter((name) => 11 | shouldPreparePackageAsync(name) 12 | ); 13 | 14 | for (const packageName of packageNames) { 15 | await prepareAsync(packageName); 16 | } 17 | } 18 | 19 | async function shouldPreparePackageAsync(packageName) { 20 | const packageJson = JSON.parse( 21 | await fs.readFile(path.join(ROOT, 'packages', packageName, 'package.json'), 'utf8') 22 | ); 23 | return !!packageJson.scripts?.prepare; 24 | } 25 | 26 | async function prepareAsync(packageName) { 27 | console.log(`⚙️ Preparing package - ${packageName}`); 28 | const packageRoot = path.join(ROOT, 'packages', packageName); 29 | await spawnAsync('bun', ['run', 'prepare'], { 30 | cwd: packageRoot, 31 | }); 32 | } 33 | 34 | (async () => { 35 | try { 36 | await runAsync(); 37 | } catch (e) { 38 | console.error('Uncaught Error', e); 39 | process.exit(1); 40 | } 41 | })(); 42 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "strictNullChecks": true, 8 | "strictPropertyInitialization": true, 9 | "strictFunctionTypes": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "noImplicitReturns": true, 13 | "allowSyntheticDefaultImports": true, 14 | "downlevelIteration": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------