rn-backstage 1.0.0 → 1.2.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 (72) hide show
  1. package/README.md +74 -23
  2. package/lib/commonjs/Backstage.js +55 -5
  3. package/lib/commonjs/Backstage.js.map +1 -1
  4. package/lib/commonjs/components/BackstagePanel.js +36 -5
  5. package/lib/commonjs/components/BackstagePanel.js.map +1 -1
  6. package/lib/commonjs/components/FlagsTab.js +255 -0
  7. package/lib/commonjs/components/FlagsTab.js.map +1 -0
  8. package/lib/commonjs/components/NetworkItem.js +537 -0
  9. package/lib/commonjs/components/NetworkItem.js.map +1 -0
  10. package/lib/commonjs/components/NetworkTab.js +274 -0
  11. package/lib/commonjs/components/NetworkTab.js.map +1 -0
  12. package/lib/commonjs/constants.js +11 -3
  13. package/lib/commonjs/constants.js.map +1 -1
  14. package/lib/commonjs/index.js +6 -0
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/log-interceptor.js +8 -1
  17. package/lib/commonjs/log-interceptor.js.map +1 -1
  18. package/lib/commonjs/network-interceptor.js +417 -0
  19. package/lib/commonjs/network-interceptor.js.map +1 -0
  20. package/lib/commonjs/types.js +9 -1
  21. package/lib/commonjs/types.js.map +1 -1
  22. package/lib/module/Backstage.js +56 -6
  23. package/lib/module/Backstage.js.map +1 -1
  24. package/lib/module/components/BackstagePanel.js +36 -5
  25. package/lib/module/components/BackstagePanel.js.map +1 -1
  26. package/lib/module/components/FlagsTab.js +250 -0
  27. package/lib/module/components/FlagsTab.js.map +1 -0
  28. package/lib/module/components/NetworkItem.js +532 -0
  29. package/lib/module/components/NetworkItem.js.map +1 -0
  30. package/lib/module/components/NetworkTab.js +269 -0
  31. package/lib/module/components/NetworkTab.js.map +1 -0
  32. package/lib/module/constants.js +10 -2
  33. package/lib/module/constants.js.map +1 -1
  34. package/lib/module/index.js +1 -1
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/log-interceptor.js +8 -1
  37. package/lib/module/log-interceptor.js.map +1 -1
  38. package/lib/module/network-interceptor.js +409 -0
  39. package/lib/module/network-interceptor.js.map +1 -0
  40. package/lib/module/types.js +10 -0
  41. package/lib/module/types.js.map +1 -1
  42. package/lib/typescript/Backstage.d.ts.map +1 -1
  43. package/lib/typescript/components/BackstagePanel.d.ts +7 -1
  44. package/lib/typescript/components/BackstagePanel.d.ts.map +1 -1
  45. package/lib/typescript/components/FlagsTab.d.ts +9 -0
  46. package/lib/typescript/components/FlagsTab.d.ts.map +1 -0
  47. package/lib/typescript/components/NetworkItem.d.ts +9 -0
  48. package/lib/typescript/components/NetworkItem.d.ts.map +1 -0
  49. package/lib/typescript/components/NetworkTab.d.ts +11 -0
  50. package/lib/typescript/components/NetworkTab.d.ts.map +1 -0
  51. package/lib/typescript/constants.d.ts +8 -0
  52. package/lib/typescript/constants.d.ts.map +1 -1
  53. package/lib/typescript/index.d.ts +2 -2
  54. package/lib/typescript/index.d.ts.map +1 -1
  55. package/lib/typescript/log-interceptor.d.ts +3 -1
  56. package/lib/typescript/log-interceptor.d.ts.map +1 -1
  57. package/lib/typescript/network-interceptor.d.ts +35 -0
  58. package/lib/typescript/network-interceptor.d.ts.map +1 -0
  59. package/lib/typescript/types.d.ts +46 -0
  60. package/lib/typescript/types.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/Backstage.tsx +64 -6
  63. package/src/components/BackstagePanel.tsx +46 -5
  64. package/src/components/FlagsTab.tsx +249 -0
  65. package/src/components/InfoTab.tsx +1 -1
  66. package/src/components/NetworkItem.tsx +536 -0
  67. package/src/components/NetworkTab.tsx +274 -0
  68. package/src/constants.ts +11 -2
  69. package/src/index.ts +3 -1
  70. package/src/log-interceptor.ts +12 -1
  71. package/src/network-interceptor.ts +468 -0
  72. package/src/types.ts +60 -0
@@ -0,0 +1,536 @@
1
+ import React, { useCallback, useState } from 'react'
2
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'
3
+ import { DarkTheme, MonospaceFont } from '../constants'
4
+ import { NetworkState } from '../types'
5
+ import type { NetworkEntry } from '../types'
6
+ import { formatTimestamp } from '../utils/formatTimestamp'
7
+ import { JsonTreeView } from './JsonTreeView'
8
+ import { toCurl } from '../network-interceptor'
9
+
10
+ // ─── Types ───────────────────────────────────────────────────────────────────
11
+
12
+ interface NetworkItemProps {
13
+ item: NetworkEntry
14
+ onCopy?: (text: string) => void
15
+ }
16
+
17
+ // ─── Status Badge Config ─────────────────────────────────────────────────────
18
+
19
+ function getStatusConfig(entry: NetworkEntry): {
20
+ label: string
21
+ color: string
22
+ bgColor: string
23
+ rowBg: string
24
+ } {
25
+ if (entry.state === NetworkState.pending) {
26
+ return {
27
+ label: '…',
28
+ color: DarkTheme.warning,
29
+ bgColor: DarkTheme.warningDim,
30
+ rowBg: 'transparent',
31
+ }
32
+ }
33
+
34
+ if (entry.state === NetworkState.error) {
35
+ return {
36
+ label: 'ERR',
37
+ color: DarkTheme.error,
38
+ bgColor: DarkTheme.errorDim,
39
+ rowBg: 'rgba(255, 77, 106, 0.06)',
40
+ }
41
+ }
42
+
43
+ const status = entry.status ?? 0
44
+
45
+ if (status >= 200 && status < 300) {
46
+ return {
47
+ label: String(status),
48
+ color: DarkTheme.success,
49
+ bgColor: 'rgba(52, 211, 153, 0.12)',
50
+ rowBg: 'transparent',
51
+ }
52
+ }
53
+
54
+ if (status >= 300 && status < 400) {
55
+ return {
56
+ label: String(status),
57
+ color: DarkTheme.info,
58
+ bgColor: DarkTheme.infoDim,
59
+ rowBg: 'transparent',
60
+ }
61
+ }
62
+
63
+ if (status >= 400 && status < 500) {
64
+ return {
65
+ label: String(status),
66
+ color: DarkTheme.warning,
67
+ bgColor: DarkTheme.warningDim,
68
+ rowBg: 'rgba(255, 178, 36, 0.06)',
69
+ }
70
+ }
71
+
72
+ // 5xx
73
+ return {
74
+ label: String(status),
75
+ color: DarkTheme.error,
76
+ bgColor: DarkTheme.errorDim,
77
+ rowBg: 'rgba(255, 77, 106, 0.06)',
78
+ }
79
+ }
80
+
81
+ // ─── Method Badge Colors ─────────────────────────────────────────────────────
82
+
83
+ function getMethodColor(method: string): string {
84
+ switch (method) {
85
+ case 'GET':
86
+ return DarkTheme.success
87
+ case 'POST':
88
+ return DarkTheme.info
89
+ case 'PUT':
90
+ case 'PATCH':
91
+ return DarkTheme.warning
92
+ case 'DELETE':
93
+ return DarkTheme.error
94
+ default:
95
+ return DarkTheme.textSecondary
96
+ }
97
+ }
98
+
99
+ // ─── URL Helpers ─────────────────────────────────────────────────────────────
100
+
101
+ function extractPath(url: string): string {
102
+ try {
103
+ const parsed = new URL(url)
104
+ const path = parsed.pathname + parsed.search
105
+ return path.length > 60 ? path.substring(0, 57) + '...' : path
106
+ } catch {
107
+ return url.length > 60 ? url.substring(0, 57) + '...' : url
108
+ }
109
+ }
110
+
111
+ function extractHost(url: string): string {
112
+ try {
113
+ return new URL(url).host
114
+ } catch {
115
+ return ''
116
+ }
117
+ }
118
+
119
+ // ─── Format Duration ─────────────────────────────────────────────────────────
120
+
121
+ function formatDuration(ms?: number): string {
122
+ if (ms === undefined) return '—'
123
+ if (ms < 1000) return `${ms}ms`
124
+ return `${(ms / 1000).toFixed(2)}s`
125
+ }
126
+
127
+ function formatSize(bytes?: number): string {
128
+ if (bytes === undefined) return ''
129
+ if (bytes < 1024) return `${bytes}B`
130
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
131
+ return `${(bytes / (1024 * 1024)).toFixed(2)}MB`
132
+ }
133
+
134
+ // ─── Try Parse JSON ──────────────────────────────────────────────────────────
135
+
136
+ function tryParseJSON(text?: string): unknown | null {
137
+ if (!text) return null
138
+ try {
139
+ return JSON.parse(text)
140
+ } catch {
141
+ return null
142
+ }
143
+ }
144
+
145
+ // ─── Component ───────────────────────────────────────────────────────────────
146
+
147
+ export const NetworkItem: React.FC<NetworkItemProps> = React.memo(({ item, onCopy }) => {
148
+ const [expanded, setExpanded] = useState(false)
149
+ const [detailSection, setDetailSection] = useState<'general' | 'request' | 'response'>('general')
150
+
151
+ const config = getStatusConfig(item)
152
+ const methodColor = getMethodColor(item.method)
153
+ const isPending = item.state === NetworkState.pending
154
+
155
+ const toggleExpand = useCallback(() => {
156
+ setExpanded(prev => !prev)
157
+ }, [])
158
+
159
+ const handleCopyCurl = useCallback(() => {
160
+ if (onCopy) {
161
+ onCopy(toCurl(item))
162
+ }
163
+ }, [item, onCopy])
164
+
165
+ return (
166
+ <TouchableOpacity
167
+ style={[styles.container, { backgroundColor: config.rowBg }]}
168
+ onPress={toggleExpand}
169
+ onLongPress={handleCopyCurl}
170
+ activeOpacity={0.7}
171
+ >
172
+ {/* ── Collapsed Row ─────────────────────────────────────── */}
173
+ <View style={styles.headerRow}>
174
+ {/* Method badge */}
175
+ <View style={[styles.methodBadge, { borderColor: methodColor }]}>
176
+ <Text style={[styles.methodText, { color: methodColor }]}>{item.method}</Text>
177
+ </View>
178
+
179
+ {/* Status badge */}
180
+ <View style={[styles.statusBadge, { backgroundColor: config.bgColor }]}>
181
+ <Text style={[styles.statusText, { color: config.color }]}>{config.label}</Text>
182
+ </View>
183
+
184
+ {/* Duration */}
185
+ {!isPending && <Text style={styles.duration}>{formatDuration(item.duration)}</Text>}
186
+ {isPending && <Text style={[styles.duration, { color: DarkTheme.warning }]}>pending</Text>}
187
+
188
+ {/* Size */}
189
+ {item.responseSize !== undefined && (
190
+ <Text style={styles.size}>{formatSize(item.responseSize)}</Text>
191
+ )}
192
+
193
+ {/* Timestamp */}
194
+ <Text style={styles.timestamp}>{formatTimestamp(item.startTime)}</Text>
195
+ </View>
196
+
197
+ {/* URL */}
198
+ <Text style={styles.path} numberOfLines={expanded ? undefined : 1}>
199
+ {extractPath(item.url)}
200
+ </Text>
201
+ <Text style={styles.host} numberOfLines={1}>
202
+ {extractHost(item.url)}
203
+ </Text>
204
+
205
+ {/* Error message */}
206
+ {item.error && (
207
+ <Text style={styles.errorText} numberOfLines={expanded ? undefined : 1}>
208
+ ✕ {item.error}
209
+ </Text>
210
+ )}
211
+
212
+ {/* ── Expanded Detail ───────────────────────────────────── */}
213
+ {expanded && (
214
+ <View style={styles.detailContainer}>
215
+ {/* Section tabs */}
216
+ <View style={styles.sectionTabs}>
217
+ {(['general', 'request', 'response'] as const).map(section => (
218
+ <TouchableOpacity
219
+ key={section}
220
+ style={[styles.sectionTab, detailSection === section && styles.sectionTabActive]}
221
+ onPress={() => setDetailSection(section)}
222
+ >
223
+ <Text
224
+ style={[
225
+ styles.sectionTabText,
226
+ detailSection === section && styles.sectionTabTextActive,
227
+ ]}
228
+ >
229
+ {section.charAt(0).toUpperCase() + section.slice(1)}
230
+ </Text>
231
+ </TouchableOpacity>
232
+ ))}
233
+ </View>
234
+
235
+ {/* Section content */}
236
+ {detailSection === 'general' && (
237
+ <View style={styles.sectionContent}>
238
+ <DetailRow label="Method" value={item.method} />
239
+ <DetailRow label="URL" value={item.url} selectable />
240
+ <DetailRow
241
+ label="Status"
242
+ value={item.status ? `${item.status} ${item.statusText || ''}` : '—'}
243
+ />
244
+ <DetailRow label="Duration" value={formatDuration(item.duration)} />
245
+ {item.responseSize !== undefined && (
246
+ <DetailRow label="Size" value={formatSize(item.responseSize)} />
247
+ )}
248
+ {item.error && <DetailRow label="Error" value={item.error} isError />}
249
+ </View>
250
+ )}
251
+
252
+ {detailSection === 'request' && (
253
+ <View style={styles.sectionContent}>
254
+ {item.requestHeaders && Object.keys(item.requestHeaders).length > 0 ? (
255
+ <>
256
+ <Text style={styles.subsectionTitle}>HEADERS</Text>
257
+ <View style={styles.jsonContainer}>
258
+ <JsonTreeView data={item.requestHeaders} hideRoot maxDepth={3} />
259
+ </View>
260
+ </>
261
+ ) : (
262
+ <Text style={styles.emptyNote}>No request headers captured</Text>
263
+ )}
264
+ {item.requestBody ? (
265
+ <>
266
+ <Text style={styles.subsectionTitle}>BODY</Text>
267
+ {tryParseJSON(item.requestBody) ? (
268
+ <View style={styles.jsonContainer}>
269
+ <JsonTreeView data={tryParseJSON(item.requestBody)} hideRoot maxDepth={5} />
270
+ </View>
271
+ ) : (
272
+ <Text style={styles.bodyText} selectable>
273
+ {item.requestBody}
274
+ </Text>
275
+ )}
276
+ </>
277
+ ) : null}
278
+ </View>
279
+ )}
280
+
281
+ {detailSection === 'response' && (
282
+ <View style={styles.sectionContent}>
283
+ {item.responseHeaders && Object.keys(item.responseHeaders).length > 0 ? (
284
+ <>
285
+ <Text style={styles.subsectionTitle}>HEADERS</Text>
286
+ <View style={styles.jsonContainer}>
287
+ <JsonTreeView data={item.responseHeaders} hideRoot maxDepth={3} />
288
+ </View>
289
+ </>
290
+ ) : (
291
+ <Text style={styles.emptyNote}>No response headers captured</Text>
292
+ )}
293
+ {item.responseBody ? (
294
+ <>
295
+ <Text style={styles.subsectionTitle}>BODY</Text>
296
+ {tryParseJSON(item.responseBody) ? (
297
+ <View style={styles.jsonContainer}>
298
+ <JsonTreeView data={tryParseJSON(item.responseBody)} hideRoot maxDepth={5} />
299
+ </View>
300
+ ) : (
301
+ <Text style={styles.bodyText} selectable>
302
+ {item.responseBody}
303
+ </Text>
304
+ )}
305
+ </>
306
+ ) : (
307
+ <Text style={styles.emptyNote}>
308
+ {isPending ? 'Awaiting response…' : 'No response body'}
309
+ </Text>
310
+ )}
311
+ </View>
312
+ )}
313
+
314
+ {/* Copy as cURL */}
315
+ {onCopy && (
316
+ <TouchableOpacity
317
+ style={styles.curlButton}
318
+ onPress={handleCopyCurl}
319
+ activeOpacity={0.7}
320
+ >
321
+ <Text style={styles.curlButtonText}>⧉ Copy as cURL</Text>
322
+ </TouchableOpacity>
323
+ )}
324
+ </View>
325
+ )}
326
+
327
+ {/* Expand hint */}
328
+ {!expanded && <Text style={styles.expandHint}>tap to inspect ▾</Text>}
329
+ </TouchableOpacity>
330
+ )
331
+ })
332
+
333
+ // ─── Detail Row ──────────────────────────────────────────────────────────────
334
+
335
+ const DetailRow: React.FC<{
336
+ label: string
337
+ value: string
338
+ selectable?: boolean
339
+ isError?: boolean
340
+ }> = ({ label, value, selectable, isError }) => (
341
+ <View style={styles.detailRow}>
342
+ <Text style={styles.detailLabel}>{label}</Text>
343
+ <Text
344
+ style={[styles.detailValue, isError && { color: DarkTheme.error }]}
345
+ numberOfLines={3}
346
+ selectable={selectable}
347
+ >
348
+ {value}
349
+ </Text>
350
+ </View>
351
+ )
352
+
353
+ // ─── Styles ──────────────────────────────────────────────────────────────────
354
+
355
+ const styles = StyleSheet.create({
356
+ container: {
357
+ paddingHorizontal: 14,
358
+ paddingVertical: 10,
359
+ borderBottomWidth: 1,
360
+ borderBottomColor: DarkTheme.border,
361
+ },
362
+ headerRow: {
363
+ flexDirection: 'row',
364
+ alignItems: 'center',
365
+ marginBottom: 4,
366
+ gap: 6,
367
+ },
368
+ methodBadge: {
369
+ borderRadius: 4,
370
+ borderWidth: 1,
371
+ paddingHorizontal: 6,
372
+ paddingVertical: 1,
373
+ },
374
+ methodText: {
375
+ fontFamily: MonospaceFont,
376
+ fontSize: 10,
377
+ fontWeight: '800',
378
+ letterSpacing: 0.5,
379
+ },
380
+ statusBadge: {
381
+ borderRadius: 4,
382
+ paddingHorizontal: 6,
383
+ paddingVertical: 2,
384
+ },
385
+ statusText: {
386
+ fontFamily: MonospaceFont,
387
+ fontSize: 10,
388
+ fontWeight: '800',
389
+ letterSpacing: 0.5,
390
+ },
391
+ duration: {
392
+ fontFamily: MonospaceFont,
393
+ fontSize: 11,
394
+ color: DarkTheme.textMuted,
395
+ },
396
+ size: {
397
+ fontFamily: MonospaceFont,
398
+ fontSize: 10,
399
+ color: DarkTheme.textMuted,
400
+ opacity: 0.7,
401
+ },
402
+ timestamp: {
403
+ fontFamily: MonospaceFont,
404
+ fontSize: 10,
405
+ color: DarkTheme.textMuted,
406
+ marginLeft: 'auto',
407
+ },
408
+ path: {
409
+ fontFamily: MonospaceFont,
410
+ fontSize: 12,
411
+ color: DarkTheme.text,
412
+ lineHeight: 18,
413
+ },
414
+ host: {
415
+ fontFamily: MonospaceFont,
416
+ fontSize: 10,
417
+ color: DarkTheme.textMuted,
418
+ marginTop: 1,
419
+ },
420
+ errorText: {
421
+ fontFamily: MonospaceFont,
422
+ fontSize: 11,
423
+ color: DarkTheme.error,
424
+ marginTop: 4,
425
+ },
426
+ expandHint: {
427
+ fontFamily: MonospaceFont,
428
+ fontSize: 10,
429
+ color: DarkTheme.textMuted,
430
+ marginTop: 4,
431
+ fontStyle: 'italic',
432
+ },
433
+ // ── Detail Section ──────────────────────────────
434
+ detailContainer: {
435
+ marginTop: 10,
436
+ paddingTop: 10,
437
+ borderTopWidth: 1,
438
+ borderTopColor: DarkTheme.border,
439
+ },
440
+ sectionTabs: {
441
+ flexDirection: 'row',
442
+ gap: 4,
443
+ marginBottom: 10,
444
+ },
445
+ sectionTab: {
446
+ paddingHorizontal: 12,
447
+ paddingVertical: 6,
448
+ borderRadius: 6,
449
+ backgroundColor: DarkTheme.surfaceElevated,
450
+ borderWidth: 1,
451
+ borderColor: DarkTheme.border,
452
+ },
453
+ sectionTabActive: {
454
+ backgroundColor: DarkTheme.accentDim,
455
+ borderColor: DarkTheme.accent,
456
+ },
457
+ sectionTabText: {
458
+ fontFamily: MonospaceFont,
459
+ fontSize: 11,
460
+ fontWeight: '600',
461
+ color: DarkTheme.textMuted,
462
+ },
463
+ sectionTabTextActive: {
464
+ color: DarkTheme.accent,
465
+ },
466
+ sectionContent: {
467
+ marginBottom: 8,
468
+ },
469
+ subsectionTitle: {
470
+ fontFamily: MonospaceFont,
471
+ fontSize: 10,
472
+ fontWeight: '700',
473
+ color: DarkTheme.textMuted,
474
+ letterSpacing: 1.2,
475
+ marginBottom: 6,
476
+ marginTop: 8,
477
+ },
478
+ jsonContainer: {
479
+ backgroundColor: DarkTheme.surfaceElevated,
480
+ borderRadius: 8,
481
+ borderWidth: 1,
482
+ borderColor: DarkTheme.border,
483
+ padding: 8,
484
+ },
485
+ bodyText: {
486
+ fontFamily: MonospaceFont,
487
+ fontSize: 11,
488
+ color: DarkTheme.text,
489
+ lineHeight: 16,
490
+ backgroundColor: DarkTheme.surfaceElevated,
491
+ borderRadius: 8,
492
+ borderWidth: 1,
493
+ borderColor: DarkTheme.border,
494
+ padding: 8,
495
+ overflow: 'hidden',
496
+ },
497
+ emptyNote: {
498
+ fontFamily: MonospaceFont,
499
+ fontSize: 11,
500
+ color: DarkTheme.textMuted,
501
+ fontStyle: 'italic',
502
+ paddingVertical: 8,
503
+ },
504
+ detailRow: {
505
+ flexDirection: 'row',
506
+ paddingVertical: 4,
507
+ },
508
+ detailLabel: {
509
+ fontFamily: MonospaceFont,
510
+ fontSize: 11,
511
+ color: DarkTheme.textMuted,
512
+ width: 70,
513
+ },
514
+ detailValue: {
515
+ fontFamily: MonospaceFont,
516
+ fontSize: 11,
517
+ color: DarkTheme.text,
518
+ flex: 1,
519
+ },
520
+ curlButton: {
521
+ marginTop: 10,
522
+ backgroundColor: DarkTheme.accentDim,
523
+ borderRadius: 8,
524
+ borderWidth: 1,
525
+ borderColor: DarkTheme.accent,
526
+ paddingVertical: 8,
527
+ paddingHorizontal: 14,
528
+ alignSelf: 'flex-start',
529
+ },
530
+ curlButtonText: {
531
+ fontFamily: MonospaceFont,
532
+ fontSize: 12,
533
+ fontWeight: '700',
534
+ color: DarkTheme.accent,
535
+ },
536
+ })