react-native-debug-toolkit 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -8,6 +8,7 @@ import { initializeDebugToolkit } from './lib'
8
8
  import { createNetworkFeature } from './lib/features/NetworkFeature'
9
9
  import { createPerformanceFeature } from './lib/features/PerformanceFeature'
10
10
  import { createConsoleLogFeature } from './lib/features/ConsoleLogFeature'
11
+ import { createZustandLogFeature, addZustandLog } from './lib/features/ZustandLogFeature'
11
12
 
12
13
  export {
13
14
  DebugToolKit,
@@ -15,6 +16,8 @@ export {
15
16
  createNetworkFeature,
16
17
  createPerformanceFeature,
17
18
  createConsoleLogFeature,
19
+ createZustandLogFeature,
20
+ addZustandLog
18
21
  }
19
22
 
20
23
  export default DebugToolKit
@@ -1,44 +1,30 @@
1
- const MAX_LOGS = 100; // Max number of console logs to store
1
+ const MAX_LOGS = 200; // Max number of console logs to store
2
2
  const logs = [];
3
3
  const originalConsole = {}; // Store original console methods
4
4
 
5
5
  const _interceptConsole = () => {
6
6
  const levels = ['log', 'info', 'warn', 'error'];
7
7
  levels.forEach(level => {
8
- if (typeof console[level] === 'function') {
9
- originalConsole[level] = console[level]; // Store original (still useful for restoration)
8
+ if (typeof console[level] === 'function') { // Check if it's actually a function
9
+ originalConsole[level] = console[level]; // Store original
10
10
 
11
11
  console[level] = (...args) => {
12
- // --- Log Recording Logic ---
12
+ // Call original console method first
13
+ originalConsole[level].apply(console, args); // Use apply for proper context
14
+
15
+ // Add log entry
13
16
  if (logs.length >= MAX_LOGS) {
14
- logs.shift();
17
+ logs.shift(); // Remove the oldest log if limit is reached
15
18
  }
16
- // Optional: Process arguments for safer storage (recommended)
17
- const processedArgs = args.map(arg => {
18
- if (arg instanceof Error) return `Error: ${arg.message}${arg.stack ? `\n${arg.stack}` : ''}`;
19
- if (arg instanceof Promise) return '[Promise]';
20
- if (typeof arg === 'function') return '[Function]';
21
- if (typeof arg === 'object' && arg !== null) {
22
- try { return JSON.stringify(arg); } catch (e) { return '[Object - Cannot Serialize]'; }
23
- }
24
- return arg;
25
- });
19
+
20
+ // Store log data
26
21
  logs.push({
27
22
  timestamp: new Date(),
28
23
  level: level,
29
- data: processedArgs, // Store processed args
24
+ data: args, // Store all arguments passed to console[level]
30
25
  });
31
- // --- End Log Recording Logic ---
32
-
33
- // --- Do NOT call the original console ---
34
- // By omitting the following line, the intercepted log message (including
35
- // the "Unhandled Promise Rejection" warning) will ONLY be stored
36
- // in this feature's 'logs' array and will NOT be passed through
37
- // to the standard developer console output (Metro, browser, etc.).
38
- //
39
- // originalConsole[level]?.apply(console, args);
40
26
 
41
- // TODO: Notify UI if needed (based on the stored log)
27
+ // TODO: Notify UI if needed
42
28
  };
43
29
  }
44
30
  });
@@ -2,7 +2,7 @@ import React from 'react'
2
2
 
3
3
  class NetworkFeature {
4
4
  static instance = null
5
- static MAX_LOGS = 100
5
+ static MAX_LOGS = 200
6
6
 
7
7
  constructor() {
8
8
  if (NetworkFeature.instance) {
@@ -0,0 +1,46 @@
1
+ const MAX_LOGS = 200; // Max number of Zustand logs to store
2
+ const logs = [];
3
+
4
+ // Zustand middleware to capture state changes
5
+ export const addZustandLog = (action, prevState, nextState, actionCompleteTime, storeName) => {
6
+ // Store log data
7
+ logs.push({
8
+ timestamp: new Date(),
9
+ action: action,
10
+ prevState: prevState,
11
+ nextState: nextState,
12
+ actionCompleteTime: actionCompleteTime,
13
+ storeName: storeName,
14
+ });
15
+
16
+ // Trim logs if they exceed the maximum limit
17
+ if (logs.length > MAX_LOGS) {
18
+ logs.splice(0, logs.length - MAX_LOGS);
19
+ }
20
+ };
21
+
22
+ const setup = () => {
23
+ if (!__DEV__) {
24
+ return;
25
+ }
26
+ // Note: The actual middleware setup happens in the store creation
27
+ // This function is mainly for compatibility with the feature API
28
+ };
29
+
30
+ const getData = () => {
31
+ return logs;
32
+ };
33
+
34
+ const cleanup = () => {
35
+ logs.length = 0; // Clear array
36
+ };
37
+
38
+ export const createZustandLogFeature = () => {
39
+ return {
40
+ name: 'zustand',
41
+ label: 'Zustand Logs',
42
+ setup: setup,
43
+ getData: getData,
44
+ cleanup: cleanup,
45
+ };
46
+ };
package/lib/index.js CHANGED
@@ -2,18 +2,20 @@ import DebugToolKit from './DebugToolKit'
2
2
  import { createNetworkFeature } from './features/NetworkFeature'
3
3
  import { createPerformanceFeature} from './features/PerformanceFeature'
4
4
  import { createConsoleLogFeature } from './features/ConsoleLogFeature'
5
+ import { createZustandLogFeature, zustandLogMiddleware } from './features/ZustandLogFeature'
5
6
 
6
7
  // Registry mapping feature names (strings) to their creator functions
7
8
  const featureRegistry = {
8
9
  network: createNetworkFeature,
9
10
  console: createConsoleLogFeature,
10
11
  performance: createPerformanceFeature,
12
+ zustand: createZustandLogFeature,
11
13
  // Add other built-in features here
12
14
  // 'storage': createStorageFeature,
13
15
  }
14
16
 
15
17
  // List of default features (can use strings now)
16
- const defaultFeatures = ['network', 'console']
18
+ const defaultFeatures = ['network', 'console', 'zustand']
17
19
 
18
20
  /**
19
21
  * Initializes and shows the Debug ToolKit panel with specified features.
@@ -73,4 +75,12 @@ export function initializeDebugToolkit(features = defaultFeatures) {
73
75
  }
74
76
 
75
77
  // Export existing stuff and the new initializer
76
- export { DebugToolKit, createNetworkFeature, createPerformanceFeature, createConsoleLogFeature, featureRegistry };
78
+ export {
79
+ DebugToolKit,
80
+ createNetworkFeature,
81
+ createPerformanceFeature,
82
+ createConsoleLogFeature,
83
+ createZustandLogFeature,
84
+ zustandLogMiddleware, // Export middleware for use in Zustand stores
85
+ featureRegistry
86
+ };
@@ -19,7 +19,7 @@ export const DebugColors = {
19
19
  export const getLogLevelColor = (level) => {
20
20
  switch (level?.toLowerCase()) {
21
21
  case 'log':
22
- return '#666';
22
+ return '#333';
23
23
  case 'info':
24
24
  return '#0D96F2';
25
25
  case 'warn':
@@ -33,3 +33,17 @@ export const getLogLevelColor = (level) => {
33
33
  export const DebugImgs = {
34
34
  iconLink: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVGhD7ZgxDoMwDEV9/xvQtReoGBASYmFhYmNiYmNhYWJiQQIJqUh8y05wlAYw+T/pD5Dg9xwnhPR6vV6v1+v1er3eGvuOb7yIhxEQvjAC4hRGQJzGCIhLGAFxGSMgbmEExC2MgLiFERC3MALiFkZA3MIIiFsYAXELIyBuYQTELYyAuIURELcwAuIWRkDcwgiIWxgBcQsjIG5hBMQtjIC4hREQtzAC4hZGQNzCCIhbGAFxCyMgbmEExC2MgLiFERC3MALiFkZA3MIIiFsYAXELIyBuYQTELYyAuIURELcwAuIWRkDcwgiIW/+PYBgfhPtL9WBrfKUAAAAASUVORK5CYII='
35
35
  };
36
+
37
+ // Function to get color for Zustand action types
38
+ export const getZustandActionColor = (action) => {
39
+ if (!action) return '#666666'; // Default gray
40
+
41
+ // You can customize this based on common Zustand action patterns
42
+ if (action.startsWith('set')) return '#0D96F2'; // Blue for setters
43
+ if (action.startsWith('update')) return '#4CAF50'; // Green for updates
44
+ if (action.startsWith('delete') || action.startsWith('remove')) return '#F93E3E'; // Red for deletions
45
+ if (action.startsWith('toggle')) return '#FCA130'; // Orange for toggles
46
+ if (action.startsWith('init')) return '#9C27B0'; // Purple for initialization
47
+
48
+ return '#0D96F2'; // Default blue for other actions
49
+ };
@@ -0,0 +1,79 @@
1
+ import { MMKV } from 'react-native-mmkv';
2
+
3
+ // Create MMKV instance for storing debug logs
4
+ const storage = new MMKV({
5
+ id: 'debug-toolkit-storage',
6
+ encryptionKey: 'debug-toolkit-encryption-key'
7
+ });
8
+
9
+ // Storage keys for different features
10
+ const STORAGE_KEYS = {
11
+ CONSOLE_LOGS: 'console_logs',
12
+ NETWORK_LOGS: 'network_logs',
13
+ PERFORMANCE_DATA: 'performance_data',
14
+ ZUSTAND_LOGS: 'zustand_logs',
15
+ };
16
+
17
+ /**
18
+ * Utility class for storing and retrieving logs using MMKV
19
+ */
20
+ export class StorageUtils {
21
+ /**
22
+ * Save data to MMKV storage
23
+ * @param {string} key - Storage key
24
+ * @param {any} data - Data to store
25
+ */
26
+ static saveData(key, data) {
27
+ try {
28
+ const jsonString = JSON.stringify(data);
29
+ storage.set(key, jsonString);
30
+ } catch (error) {
31
+ console.error(`Error saving data for key ${key}:`, error);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Retrieve data from MMKV storage
37
+ * @param {string} key - Storage key
38
+ * @param {any} defaultValue - Default value if data doesn't exist
39
+ * @returns {any} - Retrieved data or default value
40
+ */
41
+ static getData(key, defaultValue = null) {
42
+ try {
43
+ const jsonString = storage.getString(key);
44
+ if (!jsonString) return defaultValue;
45
+ return JSON.parse(jsonString);
46
+ } catch (error) {
47
+ console.error(`Error retrieving data for key ${key}:`, error);
48
+ return defaultValue;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Delete data from MMKV storage
54
+ * @param {string} key - Storage key
55
+ */
56
+ static deleteData(key) {
57
+ try {
58
+ storage.delete(key);
59
+ } catch (error) {
60
+ console.error(`Error deleting data for key ${key}:`, error);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Clear all debug toolkit data from storage
66
+ */
67
+ static clearAllData() {
68
+ try {
69
+ Object.values(STORAGE_KEYS).forEach(key => {
70
+ storage.delete(key);
71
+ });
72
+ } catch (error) {
73
+ console.error('Error clearing all data:', error);
74
+ }
75
+ }
76
+ }
77
+
78
+ // Export storage keys for use in features
79
+ export { STORAGE_KEYS };
@@ -15,6 +15,7 @@ import SubViewHTTPLogs from './SubViewHTTPLogs'
15
15
  import SubViewPerformance from './SubViewPerformance'
16
16
  import TabView from './TabView'
17
17
  import SubViewConsoleLogs from './SubViewConsoleLogs'
18
+ import SubViewZustandLogs from './SubViewZustandLogs'
18
19
 
19
20
  const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
20
21
 
@@ -281,6 +282,10 @@ export default class FloatPanelView extends Component {
281
282
  if (feature.name === 'console') {
282
283
  return <SubViewConsoleLogs logs={data} />
283
284
  }
285
+
286
+ if (feature.name === 'zustand') {
287
+ return <SubViewZustandLogs logs={data} />
288
+ }
284
289
 
285
290
  // Generic fallback for other feature types
286
291
  return (
@@ -0,0 +1,279 @@
1
+ import React, { useState, useMemo } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ FlatList,
7
+ TouchableOpacity,
8
+ } from 'react-native'
9
+ import ZustandLogDetails from './ZustandLogDetails'
10
+ import { getZustandActionColor } from '../utils/DebugConst'
11
+
12
+ const SubViewZustandLogs = ({ logs = [] }) => {
13
+ const [selectedLog, setSelectedLog] = useState(null)
14
+
15
+ // Memoize the sorted logs
16
+ const sortedLogs = useMemo(() => {
17
+ // Create a stable copy before sorting
18
+ return [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
19
+ }, [logs]);
20
+
21
+ // Helper to format preview of state changes
22
+ const formatActionPreview = (log) => {
23
+ if (!log) return '';
24
+
25
+ const { action = 'unknown', storeName } = log;
26
+ return `Action: ${action}${storeName ? ` (${storeName})` : ''}`;
27
+ };
28
+
29
+ // Safely stringify value for display
30
+ const safeStringify = (value) => {
31
+ if (value === undefined) return 'undefined';
32
+ if (value === null) return 'null';
33
+
34
+ const type = typeof value;
35
+ if (type === 'string') return `"${value.length > 10 ? value.substring(0, 10) + '...' : value}"`;
36
+ if (type === 'number' || type === 'boolean') return String(value);
37
+ if (Array.isArray(value)) return '[...]';
38
+ if (type === 'object') return '{...}';
39
+
40
+ return String(value);
41
+ };
42
+
43
+ // Helper to find key changes between states with more details
44
+ const findChangedKeys = (prevState, nextState) => {
45
+ if (!prevState || !nextState) return [];
46
+
47
+ const changes = [];
48
+ const allKeys = new Set([...Object.keys(prevState), ...Object.keys(nextState)]);
49
+
50
+ allKeys.forEach(key => {
51
+ const prevValue = prevState[key];
52
+ const nextValue = nextState[key];
53
+
54
+ // Check if key is added, removed, or value changed
55
+ if (!(key in prevState)) {
56
+ changes.push({
57
+ key,
58
+ type: 'added',
59
+ display: `+${key}: ${safeStringify(nextValue)}`
60
+ });
61
+ } else if (!(key in nextState)) {
62
+ changes.push({
63
+ key,
64
+ type: 'removed',
65
+ display: `-${key}: ${safeStringify(prevValue)}`
66
+ });
67
+ } else if (JSON.stringify(prevValue) !== JSON.stringify(nextValue)) {
68
+ // For primitive types, show the before and after values
69
+ const isPrimitive =
70
+ typeof prevValue !== 'object' ||
71
+ prevValue === null ||
72
+ typeof nextValue !== 'object' ||
73
+ nextValue === null;
74
+
75
+ if (isPrimitive) {
76
+ changes.push({
77
+ key,
78
+ type: 'changed',
79
+ display: `~${key}: ${safeStringify(prevValue)} → ${safeStringify(nextValue)}`
80
+ });
81
+ } else {
82
+ changes.push({
83
+ key,
84
+ type: 'changed',
85
+ display: `~${key}`
86
+ });
87
+ }
88
+ }
89
+ });
90
+
91
+ return changes;
92
+ };
93
+
94
+ const renderLogItem = ({ item }) => {
95
+ const actionPreview = formatActionPreview(item);
96
+ const actionColor = getZustandActionColor(item.action);
97
+ const timestamp = item.timestamp
98
+ ? new Date(item.timestamp).toLocaleTimeString([], {
99
+ hour: '2-digit',
100
+ minute: '2-digit',
101
+ second: '2-digit',
102
+ hour12: false
103
+ })
104
+ : '';
105
+
106
+ // Get changed keys with details
107
+ const changes = findChangedKeys(item.prevState, item.nextState);
108
+
109
+ // Create a formatted display of changes
110
+ const changesText = changes.length
111
+ ? changes.map(change => change.display).join(', ')
112
+ : 'No changes detected';
113
+
114
+ return (
115
+ <TouchableOpacity
116
+ style={styles.logItem}
117
+ onPress={() => setSelectedLog(item)}>
118
+ <View style={styles.logItemContainer}>
119
+ <View
120
+ style={[styles.actionIndicator, { backgroundColor: actionColor }]}
121
+ />
122
+
123
+ <View style={styles.logContent}>
124
+ <View style={styles.logHeader}>
125
+ <Text style={[styles.actionText, { color: actionColor }]}>
126
+ {actionPreview}
127
+ </Text>
128
+ <Text style={styles.time}>{timestamp}</Text>
129
+ </View>
130
+
131
+ <Text style={styles.statePathsText} numberOfLines={2}>
132
+ {changesText}
133
+ </Text>
134
+ </View>
135
+ </View>
136
+ </TouchableOpacity>
137
+ )
138
+ }
139
+
140
+ // If a log is selected, show the details view
141
+ if (selectedLog) {
142
+ const timestamp = selectedLog.timestamp
143
+ ? new Date(selectedLog.timestamp).toLocaleTimeString([], {
144
+ hour: '2-digit',
145
+ minute: '2-digit',
146
+ second: '2-digit',
147
+ hour12: false
148
+ })
149
+ : '';
150
+ const actionColor = getZustandActionColor(selectedLog.action);
151
+ const actionDisplay = selectedLog.storeName
152
+ ? `${selectedLog.action} (${selectedLog.storeName})`
153
+ : selectedLog.action || 'Unknown Action';
154
+
155
+ return (
156
+ <View style={styles.container}>
157
+ <View style={styles.detailsHeader}>
158
+ <TouchableOpacity
159
+ style={styles.backButton}
160
+ onPress={() => setSelectedLog(null)}>
161
+ <Text style={styles.backButtonText}>← Back</Text>
162
+ </TouchableOpacity>
163
+ <View style={styles.headerActionTime}>
164
+ <Text style={[styles.headerAction, { color: actionColor }]}>
165
+ {actionDisplay}
166
+ </Text>
167
+ <Text style={styles.headerTimestamp}>{timestamp}</Text>
168
+ </View>
169
+ </View>
170
+ <ZustandLogDetails log={selectedLog} />
171
+ </View>
172
+ )
173
+ }
174
+
175
+ // Otherwise show the list view
176
+ return (
177
+ <View style={styles.container}>
178
+ {sortedLogs.length === 0 ? (
179
+ <Text style={styles.emptyText}>No Zustand state changes logged yet</Text>
180
+ ) : (
181
+ <FlatList
182
+ data={sortedLogs}
183
+ renderItem={renderLogItem}
184
+ keyExtractor={(item, index) => `${item.timestamp}-${index}`}
185
+ style={styles.list}
186
+ />
187
+ )}
188
+ </View>
189
+ )
190
+ }
191
+
192
+ // Adapted styles from SubViewConsoleLogs
193
+ const styles = StyleSheet.create({
194
+ container: {
195
+ flex: 1,
196
+ backgroundColor: '#fff',
197
+ },
198
+ list: {
199
+ flex: 1,
200
+ },
201
+ emptyText: {
202
+ textAlign: 'center',
203
+ color: '#999',
204
+ marginTop: 20,
205
+ },
206
+ logItem: {
207
+ borderBottomWidth: 1,
208
+ borderBottomColor: '#eee',
209
+ },
210
+ logItemContainer: {
211
+ flexDirection: 'row',
212
+ paddingVertical: 10,
213
+ paddingHorizontal: 12,
214
+ },
215
+ actionIndicator: {
216
+ width: 4,
217
+ borderRadius: 2,
218
+ marginRight: 10,
219
+ },
220
+ logContent: {
221
+ flex: 1,
222
+ },
223
+ logHeader: {
224
+ flexDirection: 'row',
225
+ justifyContent: 'space-between',
226
+ alignItems: 'center',
227
+ marginBottom: 5,
228
+ },
229
+ actionText: {
230
+ fontSize: 13,
231
+ fontWeight: 'bold',
232
+ },
233
+ time: {
234
+ fontSize: 12,
235
+ color: '#888',
236
+ },
237
+ statePathsText: {
238
+ fontSize: 14,
239
+ color: '#555',
240
+ fontFamily: 'monospace',
241
+ },
242
+ // Details Header Styles
243
+ detailsHeader: {
244
+ flexDirection: 'row',
245
+ alignItems: 'center',
246
+ paddingVertical: 10,
247
+ paddingHorizontal: 15,
248
+ borderBottomWidth: 1,
249
+ borderBottomColor: '#eee',
250
+ backgroundColor: '#f8f9fa',
251
+ },
252
+ backButton: {
253
+ paddingHorizontal: 10,
254
+ paddingVertical: 5,
255
+ borderRadius: 4,
256
+ backgroundColor: '#eee',
257
+ marginRight: 15,
258
+ },
259
+ backButtonText: {
260
+ color: '#333',
261
+ fontWeight: '500',
262
+ fontSize: 14,
263
+ },
264
+ headerActionTime: {
265
+ flexDirection: 'row',
266
+ alignItems: 'baseline',
267
+ },
268
+ headerAction: {
269
+ fontSize: 15,
270
+ fontWeight: 'bold',
271
+ marginRight: 8,
272
+ },
273
+ headerTimestamp: {
274
+ fontSize: 13,
275
+ color: '#666',
276
+ },
277
+ })
278
+
279
+ export default SubViewZustandLogs
@@ -0,0 +1,355 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, Clipboard } from 'react-native'
3
+ import { ScrollView, Pressable } from 'react-native'
4
+ import JSONTree from 'react-native-json-tree'
5
+ import { getZustandActionColor } from '../utils/DebugConst'
6
+
7
+ // Re-using the theme from ConsoleLogDetails for consistency
8
+ const theme = {
9
+ scheme: 'monokai',
10
+ author: 'wimer hazenberg (http://www.monokai.nl)',
11
+ base00: '#272822',
12
+ base01: '#383830',
13
+ base02: '#49483e',
14
+ base03: '#75715e',
15
+ base04: '#a59f85',
16
+ base05: '#f8f8f2',
17
+ base06: '#f5f4f1',
18
+ base07: '#f9f8f5',
19
+ base08: '#f92672',
20
+ base09: '#fd971f',
21
+ base0A: '#f4bf75',
22
+ base0B: '#a6e22e',
23
+ base0C: '#a1efe4',
24
+ base0D: '#66d9ef',
25
+ base0E: '#ae81ff',
26
+ base0F: '#cc6633'
27
+ };
28
+
29
+ const CopyButton = ({ text, style }) => {
30
+ const [copied, setCopied] = React.useState(false)
31
+
32
+ const handleCopy = async () => {
33
+ await Clipboard.setString(text)
34
+ setCopied(true)
35
+ setTimeout(() => setCopied(false), 2000)
36
+ }
37
+
38
+ return (
39
+ <Pressable style={[styles.copyButton, style]} onPress={handleCopy}>
40
+ <Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy'}</Text>
41
+ </Pressable>
42
+ )
43
+ }
44
+
45
+ const CollapsibleSection = ({ title, children, initiallyExpanded = false }) => {
46
+ const [expanded, setExpanded] = React.useState(initiallyExpanded)
47
+
48
+ return (
49
+ <View style={styles.collapsibleSection}>
50
+ <Pressable
51
+ style={styles.sectionHeader}
52
+ onPress={() => setExpanded(!expanded)}>
53
+ <Text style={styles.sectionTitle}>{title}</Text>
54
+ <Text style={styles.expandIcon}>{expanded ? '▼' : '▶'}</Text>
55
+ </Pressable>
56
+ {expanded && children}
57
+ </View>
58
+ )
59
+ }
60
+
61
+ const ZustandLogDetails = ({ log }) => {
62
+ if (!log) {
63
+ return (
64
+ <View style={styles.errorContainer}>
65
+ <Text style={styles.errorText}>Log data is missing</Text>
66
+ </View>
67
+ )
68
+ }
69
+
70
+ const { timestamp, action, prevState, nextState, actionCompleteTime, storeName } = log
71
+ const actionColor = getZustandActionColor(action);
72
+
73
+ // Format data for display and copying
74
+ const formatStateToString = (state) => {
75
+ try {
76
+ return JSON.stringify(state, null, 2);
77
+ } catch (e) {
78
+ return '[unserializable state]';
79
+ }
80
+ }
81
+
82
+ const formattedPrevState = formatStateToString(prevState);
83
+ const formattedNextState = formatStateToString(nextState);
84
+
85
+ // Format for the full log copy
86
+ const formattedLog = `Action: ${action}${storeName ? ` (${storeName})` : ''}
87
+ Previous State: ${formattedPrevState}
88
+ Next State: ${formattedNextState}${actionCompleteTime ? `\nAction Complete Time: ${actionCompleteTime}ms` : ''}`;
89
+
90
+ // Function to find differences between states
91
+ const findDifferences = () => {
92
+ const changes = [];
93
+
94
+ if (!prevState || !nextState) return changes;
95
+
96
+ // Helper to find differences in objects recursively
97
+ const findChangesRecursive = (prev, next, path = '') => {
98
+ if (prev === next) return;
99
+
100
+ if (typeof prev !== 'object' || typeof next !== 'object' ||
101
+ prev === null || next === null) {
102
+ changes.push({
103
+ path: path || 'root',
104
+ prevValue: prev,
105
+ nextValue: next
106
+ });
107
+ return;
108
+ }
109
+
110
+ // Check for object keys
111
+ const allKeys = [...new Set([...Object.keys(prev || {}), ...Object.keys(next || {})])];
112
+
113
+ for (const key of allKeys) {
114
+ const newPath = path ? `${path}.${key}` : key;
115
+ if (!(key in prev)) {
116
+ changes.push({
117
+ path: newPath,
118
+ prevValue: undefined,
119
+ nextValue: next[key]
120
+ });
121
+ } else if (!(key in next)) {
122
+ changes.push({
123
+ path: newPath,
124
+ prevValue: prev[key],
125
+ nextValue: undefined
126
+ });
127
+ } else {
128
+ findChangesRecursive(prev[key], next[key], newPath);
129
+ }
130
+ }
131
+ };
132
+
133
+ findChangesRecursive(prevState, nextState);
134
+ return changes;
135
+ };
136
+
137
+ const stateChanges = findDifferences();
138
+ const hasChanges = stateChanges.length > 0;
139
+
140
+ const actionDisplay = storeName ? `${action} (${storeName})` : action;
141
+
142
+ return (
143
+ <ScrollView
144
+ style={styles.container}
145
+ contentContainerStyle={styles.contentContainer}
146
+ showsVerticalScrollIndicator={true}
147
+ scrollEventThrottle={16}
148
+ keyboardShouldPersistTaps='handled'>
149
+
150
+ <View style={styles.header}>
151
+ <View style={styles.headerInfo}>
152
+ <Text style={styles.actionIndicator}>
153
+ Action: <Text style={[styles.actionName, { color: actionColor }]}>{actionDisplay}</Text>
154
+ </Text>
155
+ <Text style={styles.timestamp}>
156
+ {timestamp
157
+ ? new Date(timestamp).toLocaleString()
158
+ : 'Unknown time'}
159
+ </Text>
160
+ {actionCompleteTime && (
161
+ <Text style={styles.actionTime}>
162
+ Completed in: {actionCompleteTime}ms
163
+ </Text>
164
+ )}
165
+ </View>
166
+ <CopyButton text={formattedLog} />
167
+ </View>
168
+
169
+ {hasChanges && (
170
+ <CollapsibleSection title='State Changes' initiallyExpanded={true}>
171
+ <View style={styles.dataContentWrapper}>
172
+ <View style={styles.dataContent}>
173
+ {stateChanges.map((change, index) => (
174
+ <View key={index} style={[styles.changeItem, index === stateChanges.length - 1 && styles.changeItemLast]}>
175
+ <Text style={styles.changePath}>{change.path}</Text>
176
+ <View style={styles.changeValues}>
177
+ <View style={styles.changeValue}>
178
+ <Text style={styles.changeLabel}>From:</Text>
179
+ <JSONTree
180
+ data={change.prevValue}
181
+ theme={theme}
182
+ invertTheme={true}
183
+ hideRoot={true}
184
+ shouldExpandNode={() => true}
185
+ />
186
+ </View>
187
+ <View style={styles.changeValue}>
188
+ <Text style={styles.changeLabel}>To:</Text>
189
+ <JSONTree
190
+ data={change.nextValue}
191
+ theme={theme}
192
+ invertTheme={true}
193
+ hideRoot={true}
194
+ shouldExpandNode={() => true}
195
+ />
196
+ </View>
197
+ </View>
198
+ </View>
199
+ ))}
200
+ </View>
201
+ </View>
202
+ </CollapsibleSection>
203
+ )}
204
+
205
+ <CollapsibleSection title='Previous State' initiallyExpanded={false}>
206
+ <View style={styles.dataContentWrapper}>
207
+ <View style={styles.dataContent}>
208
+ <JSONTree
209
+ data={prevState || {}}
210
+ theme={theme}
211
+ invertTheme={true}
212
+ hideRoot={true}
213
+ shouldExpandNode={(keyPath, data, level) => level < 1}
214
+ />
215
+ </View>
216
+ </View>
217
+ </CollapsibleSection>
218
+
219
+ <CollapsibleSection title='Next State' initiallyExpanded={true}>
220
+ <View style={styles.dataContentWrapper}>
221
+ <View style={styles.dataContent}>
222
+ <JSONTree
223
+ data={nextState || {}}
224
+ theme={theme}
225
+ invertTheme={true}
226
+ hideRoot={true}
227
+ shouldExpandNode={(keyPath, data, level) => level < 1}
228
+ />
229
+ </View>
230
+ </View>
231
+ </CollapsibleSection>
232
+
233
+ </ScrollView>
234
+ )
235
+ }
236
+
237
+ // Re-using and adapting styles from ConsoleLogDetails
238
+ const styles = StyleSheet.create({
239
+ container: {
240
+ flex: 1,
241
+ backgroundColor: '#fff',
242
+ },
243
+ contentContainer: {
244
+ paddingBottom: 20,
245
+ },
246
+ header: {
247
+ flexDirection: 'row',
248
+ padding: 15,
249
+ borderBottomWidth: 1,
250
+ borderBottomColor: '#eee',
251
+ alignItems: 'center',
252
+ justifyContent: 'space-between',
253
+ },
254
+ headerInfo: {
255
+ flexShrink: 1,
256
+ marginRight: 10,
257
+ },
258
+ actionIndicator: {
259
+ fontSize: 14,
260
+ marginBottom: 5,
261
+ },
262
+ actionName: {
263
+ fontWeight: 'bold',
264
+ },
265
+ timestamp: {
266
+ fontSize: 13,
267
+ color: '#666',
268
+ },
269
+ actionTime: {
270
+ fontSize: 13,
271
+ color: '#666',
272
+ marginTop: 2,
273
+ },
274
+ collapsibleSection: {
275
+ marginBottom: 1,
276
+ backgroundColor: '#fff',
277
+ },
278
+ sectionHeader: {
279
+ flexDirection: 'row',
280
+ justifyContent: 'space-between',
281
+ alignItems: 'center',
282
+ padding: 15,
283
+ backgroundColor: '#f5f5f5',
284
+ },
285
+ sectionTitle: {
286
+ fontSize: 15,
287
+ fontWeight: 'bold',
288
+ color: '#333',
289
+ },
290
+ expandIcon: {
291
+ fontSize: 14,
292
+ color: '#666',
293
+ },
294
+ dataContentWrapper: {
295
+ flex: 1,
296
+ padding: 10,
297
+ },
298
+ dataContent: {
299
+ backgroundColor: '#f8f9fa',
300
+ padding: 10,
301
+ borderRadius: 4,
302
+ borderWidth: 1,
303
+ borderColor: '#e9ecef',
304
+ },
305
+ errorContainer: {
306
+ flex: 1,
307
+ justifyContent: 'center',
308
+ alignItems: 'center',
309
+ padding: 20,
310
+ },
311
+ errorText: {
312
+ color: '#ff4444',
313
+ fontSize: 16,
314
+ },
315
+ copyButton: {
316
+ backgroundColor: '#e9ecef',
317
+ paddingHorizontal: 10,
318
+ paddingVertical: 5,
319
+ borderRadius: 4,
320
+ flexShrink: 0,
321
+ },
322
+ copyButtonText: {
323
+ fontSize: 12,
324
+ color: '#666',
325
+ },
326
+ changeItem: {
327
+ borderBottomWidth: 1,
328
+ borderBottomColor: '#eee',
329
+ paddingVertical: 8,
330
+ },
331
+ changeItemLast: {
332
+ borderBottomWidth: 0,
333
+ },
334
+ changePath: {
335
+ fontWeight: 'bold',
336
+ color: '#333',
337
+ marginBottom: 5,
338
+ fontFamily: 'monospace',
339
+ },
340
+ changeValues: {
341
+ flexDirection: 'row',
342
+ justifyContent: 'space-between',
343
+ },
344
+ changeValue: {
345
+ flex: 1,
346
+ padding: 5,
347
+ },
348
+ changeLabel: {
349
+ color: '#666',
350
+ fontSize: 12,
351
+ marginBottom: 3,
352
+ },
353
+ });
354
+
355
+ export default ZustandLogDetails
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-debug-toolkit",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "author": "zcj",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "@react-native-async-storage/async-storage": "^2.1.2",
26
+ "react-native-mmkv": "2.12.2",
27
27
  "react-native-json-tree": "^1.5.0",
28
28
  "react-native-performance": "5.1.2",
29
29
  "react-native-root-siblings": "^4.0.0"