@rozenite/storage-plugin 1.4.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +20 -0
  3. package/README.md +77 -0
  4. package/dist/assets/panel-C7usUotG.js +22 -0
  5. package/dist/assets/panel-DMhXYHH4.css +1 -0
  6. package/dist/panel.html +31 -0
  7. package/dist/react-native.cjs +1 -0
  8. package/dist/react-native.d.ts +4 -0
  9. package/dist/react-native.js +200 -0
  10. package/dist/rozenite.config.d.ts +7 -0
  11. package/dist/rozenite.json +1 -0
  12. package/dist/src/react-native/adapters/async-storage.d.ts +33 -0
  13. package/dist/src/react-native/adapters/index.d.ts +6 -0
  14. package/dist/src/react-native/adapters/mmkv.d.ts +13 -0
  15. package/dist/src/react-native/adapters/secure-storage.d.ts +18 -0
  16. package/dist/src/react-native/is-garbled.d.ts +1 -0
  17. package/dist/src/react-native/storage-view.d.ts +19 -0
  18. package/dist/src/react-native/useRozeniteStoragePlugin.d.ts +6 -0
  19. package/dist/src/shared/messaging.d.ts +29 -0
  20. package/dist/src/shared/types.d.ts +60 -0
  21. package/dist/src/ui/add-entry-dialog.d.ts +9 -0
  22. package/dist/src/ui/confirm-dialog.d.ts +11 -0
  23. package/dist/src/ui/edit-entry-dialog.d.ts +9 -0
  24. package/dist/src/ui/editable-table.d.ts +10 -0
  25. package/dist/src/ui/entry-detail-dialog.d.ts +8 -0
  26. package/dist/src/ui/panel.d.ts +1 -0
  27. package/dist/types.cjs +1 -0
  28. package/dist/types.js +11 -0
  29. package/dist/useRozeniteStoragePlugin.cjs +1 -0
  30. package/dist/useRozeniteStoragePlugin.js +212 -0
  31. package/package.json +59 -0
  32. package/postcss.config.js +6 -0
  33. package/react-native.ts +40 -0
  34. package/rozenite.config.ts +8 -0
  35. package/src/css-modules.d.ts +4 -0
  36. package/src/react-native/adapters/async-storage.ts +115 -0
  37. package/src/react-native/adapters/index.ts +19 -0
  38. package/src/react-native/adapters/mmkv.ts +187 -0
  39. package/src/react-native/adapters/secure-storage.ts +76 -0
  40. package/src/react-native/is-garbled.ts +17 -0
  41. package/src/react-native/storage-view.ts +245 -0
  42. package/src/react-native/useRozeniteStoragePlugin.ts +162 -0
  43. package/src/shared/messaging.ts +37 -0
  44. package/src/shared/types.ts +66 -0
  45. package/src/ui/add-entry-dialog.tsx +310 -0
  46. package/src/ui/confirm-dialog.tsx +100 -0
  47. package/src/ui/edit-entry-dialog.tsx +280 -0
  48. package/src/ui/editable-table.tsx +298 -0
  49. package/src/ui/entry-detail-dialog.tsx +220 -0
  50. package/src/ui/globals.css +123 -0
  51. package/src/ui/panel.tsx +442 -0
  52. package/tailwind.config.ts +94 -0
  53. package/tsconfig.json +36 -0
  54. package/vite.config.ts +20 -0
@@ -0,0 +1,442 @@
1
+ import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { Search, Plus } from 'lucide-react';
4
+ import type {
5
+ StorageDeleteEntryEvent,
6
+ StorageEventMap,
7
+ StorageSetEntryEvent,
8
+ StorageSnapshotEvent,
9
+ } from '../shared/messaging';
10
+ import type {
11
+ StorageCapabilities,
12
+ StorageEntry,
13
+ StorageEntryValue,
14
+ StorageTarget,
15
+ } from '../shared/types';
16
+ import { getStorageViewId } from '../shared/types';
17
+ import { EditableTable } from './editable-table';
18
+ import { AddEntryDialog } from './add-entry-dialog';
19
+ import { EntryDetailDialog } from './entry-detail-dialog';
20
+ import { EditEntryDialog } from './edit-entry-dialog';
21
+ import './globals.css';
22
+
23
+ type StorageSnapshotState = {
24
+ target: StorageTarget;
25
+ adapterName: string;
26
+ storageName: string;
27
+ capabilities: StorageCapabilities;
28
+ entries: StorageEntry[];
29
+ };
30
+
31
+ const getEntryTypeFromValue = (value: StorageEntryValue): StorageEntry['type'] => {
32
+ if (typeof value === 'string') {
33
+ return 'string';
34
+ }
35
+
36
+ if (typeof value === 'number') {
37
+ return 'number';
38
+ }
39
+
40
+ if (typeof value === 'boolean') {
41
+ return 'boolean';
42
+ }
43
+
44
+ return 'buffer';
45
+ };
46
+
47
+ export default function StoragePanel() {
48
+ const [snapshots, setSnapshots] = useState<Map<string, StorageSnapshotState>>(
49
+ new Map()
50
+ );
51
+ const [selectedStorageViewId, setSelectedStorageViewId] = useState<string | null>(
52
+ null
53
+ );
54
+ const [loading, setLoading] = useState(false);
55
+ const [searchTerm, setSearchTerm] = useState('');
56
+ const [showAddDialog, setShowAddDialog] = useState(false);
57
+ const [selectedEntry, setSelectedEntry] = useState<StorageEntry | null>(null);
58
+ const [showDetailDialog, setShowDetailDialog] = useState(false);
59
+ const [editingEntry, setEditingEntry] = useState<StorageEntry | null>(null);
60
+ const [showEditDialog, setShowEditDialog] = useState(false);
61
+
62
+ const client = useRozeniteDevToolsClient<StorageEventMap>({
63
+ pluginId: '@rozenite/storage-plugin',
64
+ });
65
+
66
+ useEffect(() => {
67
+ if (!client) {
68
+ return;
69
+ }
70
+
71
+ const snapshotSubscription = client.onMessage(
72
+ 'snapshot',
73
+ (event: StorageSnapshotEvent) => {
74
+ const viewId = getStorageViewId(event.target);
75
+ setSnapshots((previous) => {
76
+ const next = new Map(previous);
77
+ next.set(viewId, {
78
+ target: event.target,
79
+ adapterName: event.adapterName,
80
+ storageName: event.storageName,
81
+ capabilities: event.capabilities,
82
+ entries: event.entries,
83
+ });
84
+
85
+ if (previous.size === 0 && !selectedStorageViewId) {
86
+ setSelectedStorageViewId(viewId);
87
+ }
88
+
89
+ return next;
90
+ });
91
+
92
+ if (viewId === selectedStorageViewId) {
93
+ setLoading(false);
94
+ }
95
+ }
96
+ );
97
+
98
+ const setEntrySubscription = client.onMessage(
99
+ 'set-entry',
100
+ (event: StorageSetEntryEvent) => {
101
+ const viewId = getStorageViewId(event.target);
102
+ setSnapshots((previous) => {
103
+ const next = new Map(previous);
104
+ const current = next.get(viewId);
105
+
106
+ if (!current) {
107
+ return previous;
108
+ }
109
+
110
+ const existingIndex = current.entries.findIndex(
111
+ (entry) => entry.key === event.entry.key
112
+ );
113
+
114
+ const entries =
115
+ existingIndex >= 0
116
+ ? current.entries.map((entry) =>
117
+ entry.key === event.entry.key ? event.entry : entry
118
+ )
119
+ : [...current.entries, event.entry];
120
+
121
+ next.set(viewId, {
122
+ ...current,
123
+ entries,
124
+ });
125
+
126
+ return next;
127
+ });
128
+ }
129
+ );
130
+
131
+ const deleteEntrySubscription = client.onMessage(
132
+ 'delete-entry',
133
+ (event: StorageDeleteEntryEvent) => {
134
+ const viewId = getStorageViewId(event.target);
135
+
136
+ setSnapshots((previous) => {
137
+ const next = new Map(previous);
138
+ const current = next.get(viewId);
139
+
140
+ if (!current) {
141
+ return previous;
142
+ }
143
+
144
+ next.set(viewId, {
145
+ ...current,
146
+ entries: current.entries.filter((entry) => entry.key !== event.key),
147
+ });
148
+
149
+ return next;
150
+ });
151
+ }
152
+ );
153
+
154
+ client.send('get-snapshot', {
155
+ type: 'get-snapshot',
156
+ target: 'all',
157
+ });
158
+
159
+ return () => {
160
+ snapshotSubscription.remove();
161
+ setEntrySubscription.remove();
162
+ deleteEntrySubscription.remove();
163
+ };
164
+ }, [client, selectedStorageViewId]);
165
+
166
+ useEffect(() => {
167
+ if (!client || !selectedStorageViewId) {
168
+ return;
169
+ }
170
+
171
+ const selectedSnapshot = snapshots.get(selectedStorageViewId);
172
+
173
+ if (selectedSnapshot) {
174
+ setLoading(false);
175
+ return;
176
+ }
177
+
178
+ const separatorIndex = selectedStorageViewId.indexOf(':');
179
+ if (separatorIndex < 0) {
180
+ console.warn(
181
+ `[Rozenite] Storage Plugin: Invalid storage view id "${selectedStorageViewId}".`
182
+ );
183
+ setLoading(false);
184
+ return;
185
+ }
186
+
187
+ const adapterId = selectedStorageViewId.slice(0, separatorIndex);
188
+ const storageId = selectedStorageViewId.slice(separatorIndex + 1);
189
+
190
+ setLoading(true);
191
+ client.send('get-snapshot', {
192
+ type: 'get-snapshot',
193
+ target: {
194
+ adapterId,
195
+ storageId,
196
+ },
197
+ });
198
+ }, [client, selectedStorageViewId, snapshots]);
199
+
200
+ const selectedStorage = selectedStorageViewId
201
+ ? snapshots.get(selectedStorageViewId) ?? null
202
+ : null;
203
+
204
+ const entries = selectedStorage?.entries ?? [];
205
+
206
+ const filteredEntries = useMemo(
207
+ () =>
208
+ entries.filter((entry) =>
209
+ entry.key.toLowerCase().includes(searchTerm.toLowerCase())
210
+ ),
211
+ [entries, searchTerm]
212
+ );
213
+
214
+ const supportedTypes = selectedStorage?.capabilities.supportedTypes ?? [];
215
+
216
+ const updateEntriesForSelectedStorage = (
217
+ mutate: (entries: StorageEntry[]) => StorageEntry[]
218
+ ) => {
219
+ if (!selectedStorageViewId) {
220
+ return;
221
+ }
222
+
223
+ setSnapshots((previous) => {
224
+ const next = new Map(previous);
225
+ const current = next.get(selectedStorageViewId);
226
+
227
+ if (!current) {
228
+ return previous;
229
+ }
230
+
231
+ next.set(selectedStorageViewId, {
232
+ ...current,
233
+ entries: mutate(current.entries),
234
+ });
235
+
236
+ return next;
237
+ });
238
+ };
239
+
240
+ const handleValueChange = (key: string, newValue: StorageEntryValue) => {
241
+ if (!client || !selectedStorage) {
242
+ return;
243
+ }
244
+
245
+ const type = getEntryTypeFromValue(newValue);
246
+
247
+ if (!selectedStorage.capabilities.supportedTypes.includes(type)) {
248
+ return;
249
+ }
250
+
251
+ let updatedEntry: StorageEntry;
252
+ if (type === 'string') {
253
+ updatedEntry = { key, type: 'string', value: newValue as string };
254
+ } else if (type === 'number') {
255
+ updatedEntry = { key, type: 'number', value: newValue as number };
256
+ } else if (type === 'boolean') {
257
+ updatedEntry = { key, type: 'boolean', value: newValue as boolean };
258
+ } else {
259
+ updatedEntry = { key, type: 'buffer', value: newValue as number[] };
260
+ }
261
+
262
+ client.send('set-entry', {
263
+ type: 'set-entry',
264
+ target: selectedStorage.target,
265
+ entry: updatedEntry,
266
+ });
267
+
268
+ updateEntriesForSelectedStorage((currentEntries) =>
269
+ currentEntries.map((entry) => (entry.key === key ? updatedEntry : entry))
270
+ );
271
+ };
272
+
273
+ const handleDeleteEntry = (key: string) => {
274
+ if (!client || !selectedStorage) {
275
+ return;
276
+ }
277
+
278
+ client.send('delete-entry', {
279
+ type: 'delete-entry',
280
+ target: selectedStorage.target,
281
+ key,
282
+ });
283
+
284
+ updateEntriesForSelectedStorage((currentEntries) =>
285
+ currentEntries.filter((entry) => entry.key !== key)
286
+ );
287
+ };
288
+
289
+ const handleAddEntry = (entry: StorageEntry) => {
290
+ if (!client || !selectedStorage) {
291
+ return;
292
+ }
293
+
294
+ client.send('set-entry', {
295
+ type: 'set-entry',
296
+ target: selectedStorage.target,
297
+ entry,
298
+ });
299
+
300
+ updateEntriesForSelectedStorage((currentEntries) => [...currentEntries, entry]);
301
+ };
302
+
303
+ const storageOptions = [...snapshots.entries()].map(([viewId, snapshot]) => ({
304
+ viewId,
305
+ label: `${snapshot.adapterName} / ${snapshot.storageName}`,
306
+ }));
307
+
308
+ return (
309
+ <div className="h-screen bg-gray-900 text-gray-100 flex flex-col">
310
+ <div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
311
+ <div className="flex items-center gap-2">
312
+ <span className="text-sm font-medium text-gray-200">Storage</span>
313
+ </div>
314
+ <div className="flex-1" />
315
+ <div className="flex items-center gap-2">
316
+ <label htmlFor="storage-select" className="text-xs text-gray-400">
317
+ Storage:
318
+ </label>
319
+ <select
320
+ id="storage-select"
321
+ value={selectedStorageViewId ?? ''}
322
+ onChange={(event) => setSelectedStorageViewId(event.target.value)}
323
+ disabled={snapshots.size === 0}
324
+ className="h-8 px-2 text-xs bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
325
+ >
326
+ {snapshots.size === 0 ? (
327
+ <option>No storages found</option>
328
+ ) : (
329
+ storageOptions.map((option) => (
330
+ <option key={option.viewId} value={option.viewId}>
331
+ {option.label}
332
+ </option>
333
+ ))
334
+ )}
335
+ </select>
336
+ </div>
337
+ </div>
338
+
339
+ <div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
340
+ <button
341
+ onClick={() => setShowAddDialog(true)}
342
+ disabled={!selectedStorage}
343
+ className="flex items-center gap-1 px-3 h-8 text-xs bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded transition-colors"
344
+ title="Add new entry"
345
+ >
346
+ <Plus className="h-3 w-3" />
347
+ Add Entry
348
+ </button>
349
+ <div className="flex-1">
350
+ <div className="relative">
351
+ <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
352
+ <input
353
+ type="text"
354
+ placeholder="Search keys..."
355
+ value={searchTerm}
356
+ onChange={(event) => setSearchTerm(event.target.value)}
357
+ className="h-8 w-full pl-8 pr-3 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
358
+ />
359
+ </div>
360
+ </div>
361
+ <div className="flex items-center gap-2 text-xs text-gray-400">
362
+ {filteredEntries.length} of {entries.length} entries
363
+ </div>
364
+ </div>
365
+
366
+ <main className="flex flex-1 min-h-0 overflow-auto">
367
+ {selectedStorage ? (
368
+ filteredEntries.length === 0 ? (
369
+ <div className="flex flex-col items-center justify-center h-full text-center w-full">
370
+ <h3 className="text-lg font-semibold text-gray-200 mb-2">
371
+ No entries found
372
+ </h3>
373
+ <p className="text-gray-400 text-sm">
374
+ {searchTerm
375
+ ? 'Try adjusting your search terms'
376
+ : 'This storage appears to be empty'}
377
+ </p>
378
+ </div>
379
+ ) : (
380
+ <EditableTable
381
+ data={filteredEntries}
382
+ supportedTypes={supportedTypes}
383
+ onValueChange={handleValueChange}
384
+ onDeleteEntry={handleDeleteEntry}
385
+ onRowClick={(entry) => {
386
+ setSelectedEntry(entry);
387
+ setShowDetailDialog(true);
388
+ }}
389
+ loading={loading}
390
+ />
391
+ )
392
+ ) : (
393
+ <div className="flex flex-col items-center justify-center h-full text-center w-full">
394
+ <h2 className="text-xl font-semibold text-gray-200 mb-2">
395
+ Welcome to Storage Inspector
396
+ </h2>
397
+ <p className="text-gray-400 text-sm">
398
+ Select a storage from the dropdown above to inspect data
399
+ </p>
400
+ </div>
401
+ )}
402
+ </main>
403
+
404
+ <AddEntryDialog
405
+ isOpen={showAddDialog}
406
+ onClose={() => setShowAddDialog(false)}
407
+ onAddEntry={handleAddEntry}
408
+ existingKeys={entries.map((entry) => entry.key)}
409
+ supportedTypes={supportedTypes}
410
+ />
411
+
412
+ <EntryDetailDialog
413
+ isOpen={showDetailDialog}
414
+ onClose={() => {
415
+ setShowDetailDialog(false);
416
+ setSelectedEntry(null);
417
+ }}
418
+ onEdit={(entry) => {
419
+ setShowDetailDialog(false);
420
+ setEditingEntry(entry);
421
+ setShowEditDialog(true);
422
+ }}
423
+ entry={selectedEntry}
424
+ />
425
+
426
+ <EditEntryDialog
427
+ isOpen={showEditDialog}
428
+ onClose={() => {
429
+ setShowEditDialog(false);
430
+ setEditingEntry(null);
431
+ }}
432
+ onEditEntry={(key, newValue) => {
433
+ handleValueChange(key, newValue);
434
+ setShowEditDialog(false);
435
+ setEditingEntry(null);
436
+ }}
437
+ supportedTypes={supportedTypes}
438
+ entry={editingEntry}
439
+ />
440
+ </div>
441
+ );
442
+ }
@@ -0,0 +1,94 @@
1
+ import type { Config } from 'tailwindcss';
2
+
3
+ const config: Config = {
4
+ darkMode: ['class'],
5
+ content: ['./src/ui/**/*.{js,ts,jsx,tsx,mdx}'],
6
+ theme: {
7
+ extend: {
8
+ translate: {
9
+ '0.75': '0.1875rem',
10
+ },
11
+ colors: {
12
+ background: 'hsl(var(--background))',
13
+ foreground: 'hsl(var(--foreground))',
14
+ card: {
15
+ DEFAULT: 'hsl(var(--card))',
16
+ foreground: 'hsl(var(--card-foreground))',
17
+ },
18
+ popover: {
19
+ DEFAULT: 'hsl(var(--popover))',
20
+ foreground: 'hsl(var(--popover-foreground))',
21
+ },
22
+ primary: {
23
+ DEFAULT: 'hsl(var(--primary))',
24
+ foreground: 'hsl(var(--primary-foreground))',
25
+ },
26
+ secondary: {
27
+ DEFAULT: 'hsl(var(--secondary))',
28
+ foreground: 'hsl(var(--secondary-foreground))',
29
+ },
30
+ muted: {
31
+ DEFAULT: 'hsl(var(--muted))',
32
+ foreground: 'hsl(var(--muted-foreground))',
33
+ },
34
+ accent: {
35
+ DEFAULT: 'hsl(var(--accent))',
36
+ foreground: 'hsl(var(--accent-foreground))',
37
+ },
38
+ destructive: {
39
+ DEFAULT: 'hsl(var(--destructive))',
40
+ foreground: 'hsl(var(--destructive-foreground))',
41
+ },
42
+ border: 'hsl(var(--border))',
43
+ input: 'hsl(var(--input))',
44
+ ring: 'hsl(var(--ring))',
45
+ chart: {
46
+ '1': 'hsl(var(--chart-1))',
47
+ '2': 'hsl(var(--chart-2))',
48
+ '3': 'hsl(var(--chart-3))',
49
+ '4': 'hsl(var(--chart-4))',
50
+ '5': 'hsl(var(--chart-5))',
51
+ },
52
+ sidebar: {
53
+ DEFAULT: 'hsl(var(--sidebar-background))',
54
+ foreground: 'hsl(var(--sidebar-foreground))',
55
+ primary: 'hsl(var(--sidebar-primary))',
56
+ 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
57
+ accent: 'hsl(var(--sidebar-accent))',
58
+ 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
59
+ border: 'hsl(var(--sidebar-border))',
60
+ ring: 'hsl(var(--sidebar-ring))',
61
+ },
62
+ },
63
+ borderRadius: {
64
+ lg: 'var(--radius)',
65
+ md: 'calc(var(--radius) - 2px)',
66
+ sm: 'calc(var(--radius) - 4px)',
67
+ },
68
+ keyframes: {
69
+ 'accordion-down': {
70
+ from: {
71
+ height: '0',
72
+ },
73
+ to: {
74
+ height: 'var(--radix-accordion-content-height)',
75
+ },
76
+ },
77
+ 'accordion-up': {
78
+ from: {
79
+ height: 'var(--radix-accordion-content-height)',
80
+ },
81
+ to: {
82
+ height: '0',
83
+ },
84
+ },
85
+ },
86
+ animation: {
87
+ 'accordion-down': 'accordion-down 0.2s ease-out',
88
+ 'accordion-up': 'accordion-up 0.2s ease-out',
89
+ },
90
+ },
91
+ },
92
+ plugins: [require('tailwindcss-animate')],
93
+ };
94
+ export default config;
package/tsconfig.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "esModuleInterop": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "strict": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "bundler",
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "@rozenite/plugin-bridge": ["../plugin-bridge/src/index.ts"]
17
+ },
18
+ "resolveJsonModule": true,
19
+ "isolatedModules": true,
20
+ "noEmit": true,
21
+ "jsx": "react-jsx"
22
+ },
23
+ "include": ["src/**/*", "react-native.ts", "rozenite.config.ts"],
24
+ "exclude": ["node_modules", "dist", "build"],
25
+ "references": [
26
+ {
27
+ "path": "../plugin-bridge"
28
+ },
29
+ {
30
+ "path": "../cli"
31
+ },
32
+ {
33
+ "path": "../vite-plugin"
34
+ }
35
+ ]
36
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,20 @@
1
+ /// <reference types='vitest' />
2
+ import { defineConfig } from 'vite';
3
+ import { rozenitePlugin } from '@rozenite/vite-plugin';
4
+
5
+ export default defineConfig({
6
+ root: __dirname,
7
+ plugins: [rozenitePlugin()],
8
+ base: './',
9
+ build: {
10
+ outDir: './dist',
11
+ emptyOutDir: false,
12
+ reportCompressedSize: false,
13
+ minify: true,
14
+ sourcemap: false,
15
+ },
16
+ server: {
17
+ port: 3000,
18
+ open: true,
19
+ },
20
+ });