@rozenite/network-activity-plugin 1.0.0-alpha.8 → 1.0.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 (160) hide show
  1. package/README.md +2 -0
  2. package/dist/App.html +2 -2
  3. package/dist/assets/{App-R2ZMH9wJ.css → App-BrSkOkws.css} +269 -2
  4. package/dist/assets/{App-lNMijPJ4.js → App-C6wCDVkW.js} +17485 -10814
  5. package/dist/event-source.cjs +22 -0
  6. package/dist/event-source.js +23 -0
  7. package/dist/react-native.cjs +4 -1
  8. package/dist/react-native.js +4 -1
  9. package/dist/rozenite.json +1 -1
  10. package/dist/src/react-native/config.d.ts +20 -0
  11. package/dist/src/react-native/{network-inspector.d.ts → http/network-inspector.d.ts} +1 -1
  12. package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
  13. package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +7 -1
  14. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  15. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  16. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  17. package/dist/src/react-native/sse/types.d.ts +6 -0
  18. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
  19. package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
  20. package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
  21. package/dist/src/react-native/utils.d.ts +6 -0
  22. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  23. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  24. package/dist/src/shared/client.d.ts +53 -6
  25. package/dist/src/shared/sse-events.d.ts +38 -0
  26. package/dist/src/shared/websocket-events.d.ts +60 -0
  27. package/dist/src/ui/components/Badge.d.ts +1 -1
  28. package/dist/src/ui/components/Button.d.ts +2 -2
  29. package/dist/src/ui/components/CodeBlock.d.ts +3 -0
  30. package/dist/src/ui/components/CodeEditor.d.ts +5 -0
  31. package/dist/src/ui/components/CookieCard.d.ts +7 -0
  32. package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
  33. package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
  34. package/dist/src/ui/components/FilterBar.d.ts +10 -0
  35. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  36. package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
  37. package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
  38. package/dist/src/ui/components/RequestBody.d.ts +6 -0
  39. package/dist/src/ui/components/RequestList.d.ts +13 -28
  40. package/dist/src/ui/components/ScrollArea.d.ts +3 -2
  41. package/dist/src/ui/components/Section.d.ts +8 -0
  42. package/dist/src/ui/components/Separator.d.ts +2 -1
  43. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  44. package/dist/src/ui/components/Tabs.d.ts +7 -0
  45. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  46. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  47. package/dist/src/ui/state/derived.d.ts +5 -0
  48. package/dist/src/ui/state/hooks.d.ts +21 -0
  49. package/dist/src/ui/state/model.d.ts +103 -0
  50. package/dist/src/ui/state/store.d.ts +48 -0
  51. package/dist/src/ui/tabs/CookiesTab.d.ts +3 -6
  52. package/dist/src/ui/tabs/HeadersTab.d.ts +3 -15
  53. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  54. package/dist/src/ui/tabs/RequestTab.d.ts +2 -7
  55. package/dist/src/ui/tabs/ResponseTab.d.ts +2 -8
  56. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  57. package/dist/src/ui/tabs/TimingTab.d.ts +3 -5
  58. package/dist/src/ui/types.d.ts +4 -1
  59. package/dist/src/ui/utils/assert.d.ts +1 -0
  60. package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
  61. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  62. package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
  63. package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
  64. package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
  65. package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
  66. package/dist/src/ui/utils/getId.d.ts +1 -0
  67. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  68. package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
  69. package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
  70. package/dist/src/utils/cookieParser.d.ts +6 -0
  71. package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
  72. package/dist/src/utils/getHttpHeader.d.ts +5 -0
  73. package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
  74. package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
  75. package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
  76. package/dist/src/utils/safeStringify.d.ts +1 -0
  77. package/dist/src/utils/typeChecks.d.ts +9 -0
  78. package/dist/useNetworkActivityDevTools.cjs +724 -40
  79. package/dist/useNetworkActivityDevTools.js +723 -41
  80. package/package.json +22 -8
  81. package/react-native.ts +6 -1
  82. package/src/react-native/config.ts +43 -0
  83. package/src/react-native/http/network-inspector.ts +388 -0
  84. package/src/react-native/http/overrides-registry.ts +32 -0
  85. package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +19 -2
  86. package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
  87. package/src/react-native/sse/event-source.ts +25 -0
  88. package/src/react-native/sse/sse-inspector.ts +139 -0
  89. package/src/react-native/sse/sse-interceptor.ts +180 -0
  90. package/src/react-native/sse/types.ts +9 -0
  91. package/src/react-native/useNetworkActivityDevTools.ts +156 -4
  92. package/src/react-native/utils/getBlobName.ts +45 -0
  93. package/src/react-native/utils/getFormDataEntries.ts +32 -0
  94. package/src/react-native/utils.ts +43 -0
  95. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  96. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  97. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  98. package/src/shared/client.ts +79 -6
  99. package/src/shared/sse-events.ts +47 -0
  100. package/src/shared/websocket-events.ts +79 -0
  101. package/src/ui/components/Button.tsx +1 -0
  102. package/src/ui/components/CodeBlock.tsx +19 -0
  103. package/src/ui/components/CodeEditor.tsx +26 -0
  104. package/src/ui/components/CookieCard.tsx +64 -0
  105. package/src/ui/components/CopyRequestDropdown.tsx +95 -0
  106. package/src/ui/components/DropdownMenu.tsx +206 -0
  107. package/src/ui/components/FilterBar.tsx +117 -0
  108. package/src/ui/components/Input.tsx +1 -1
  109. package/src/ui/components/JsonTree.tsx +20 -0
  110. package/src/ui/components/JsonTreeCopyableItem.tsx +37 -0
  111. package/src/ui/components/KeyValueGrid.tsx +51 -0
  112. package/src/ui/components/OverrideResponse.tsx +132 -0
  113. package/src/ui/components/RequestBody.tsx +86 -0
  114. package/src/ui/components/RequestList.tsx +101 -131
  115. package/src/ui/components/ScrollArea.tsx +1 -0
  116. package/src/ui/components/Section.tsx +46 -0
  117. package/src/ui/components/SidePanel.tsx +333 -0
  118. package/src/ui/components/Tabs.tsx +1 -1
  119. package/src/ui/components/Toolbar.tsx +45 -0
  120. package/src/ui/globals.css +4 -0
  121. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  122. package/src/ui/state/derived.ts +112 -0
  123. package/src/ui/state/hooks.ts +52 -0
  124. package/src/ui/state/model.ts +140 -0
  125. package/src/ui/state/store.ts +669 -0
  126. package/src/ui/tabs/CookiesTab.tsx +61 -278
  127. package/src/ui/tabs/HeadersTab.tsx +85 -103
  128. package/src/ui/tabs/MessagesTab.tsx +276 -0
  129. package/src/ui/tabs/RequestTab.tsx +58 -51
  130. package/src/ui/tabs/ResponseTab.tsx +101 -74
  131. package/src/ui/tabs/SSEMessagesTab.tsx +224 -0
  132. package/src/ui/tabs/TimingTab.tsx +30 -43
  133. package/src/ui/types.ts +4 -1
  134. package/src/ui/utils/assert.ts +5 -0
  135. package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
  136. package/src/ui/utils/copyToClipboard.ts +3 -0
  137. package/src/ui/utils/escapeShellArg.ts +12 -0
  138. package/src/ui/utils/generateCurlCommand.ts +83 -0
  139. package/src/ui/utils/generateFetchCall.ts +64 -0
  140. package/src/ui/utils/generateMultipartBody.ts +19 -0
  141. package/src/ui/utils/getId.ts +10 -0
  142. package/src/ui/utils/getStatusColor.ts +15 -0
  143. package/src/ui/views/InspectorView.tsx +35 -319
  144. package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
  145. package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
  146. package/src/utils/cookieParser.ts +126 -0
  147. package/src/utils/getContentTypeMimeType.ts +17 -0
  148. package/src/utils/getHttpHeader.ts +17 -0
  149. package/src/utils/getHttpHeaderValueAsString.ts +13 -0
  150. package/src/utils/getStringSizeInBytes.ts +3 -0
  151. package/src/utils/inferContentTypeFromPostData.ts +9 -0
  152. package/src/utils/safeStringify.ts +7 -0
  153. package/src/utils/typeChecks.ts +27 -0
  154. package/tailwind.config.ts +3 -0
  155. package/vite.config.ts +12 -0
  156. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
  157. package/src/react-native/network-inspector.ts +0 -247
  158. package/src/ui/utils/getHttpHeaderValue.ts +0 -14
  159. /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
  160. /package/src/react-native/{network-requests-registry.ts → http/network-requests-registry.ts} +0 -0
@@ -0,0 +1,224 @@
1
+ import { useState, useMemo } from 'react';
2
+ import {
3
+ createColumnHelper,
4
+ flexRender,
5
+ getCoreRowModel,
6
+ useReactTable,
7
+ } from '@tanstack/react-table';
8
+ import { ScrollArea } from '../components/ScrollArea';
9
+ import { JsonTree } from '../components/JsonTree';
10
+ import { SSENetworkEntry } from '../state/model';
11
+
12
+ export type SSEMessagesTabProps = {
13
+ selectedRequest: SSENetworkEntry;
14
+ };
15
+
16
+ interface SSEMessageRow {
17
+ id: string;
18
+ type: string;
19
+ data: string;
20
+ timestamp: number;
21
+ }
22
+
23
+ const columnHelper = createColumnHelper<SSEMessageRow>();
24
+
25
+ const formatPreviewData = (data: string) => {
26
+ return (
27
+ <span className="max-w-xs truncate text-gray-400">
28
+ {data.substring(0, 100) + (data.length > 100 ? '...' : '')}
29
+ </span>
30
+ );
31
+ };
32
+
33
+ const formatTimestamp = (timestamp: number) => {
34
+ const date = new Date(timestamp);
35
+ const timeString = date.toLocaleTimeString('en-US', {
36
+ hour12: false,
37
+ hour: '2-digit',
38
+ minute: '2-digit',
39
+ second: '2-digit',
40
+ });
41
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
42
+ return `${timeString}.${milliseconds}`;
43
+ };
44
+
45
+ const columns = [
46
+ columnHelper.accessor('timestamp', {
47
+ header: 'Timestamp',
48
+ cell: ({ getValue }) => (
49
+ <div className="text-gray-400">{formatTimestamp(getValue())}</div>
50
+ ),
51
+ size: 120,
52
+ }),
53
+ columnHelper.accessor('type', {
54
+ header: 'Type',
55
+ cell: ({ getValue }) => (
56
+ <div className="text-purple-400 font-medium">{getValue()}</div>
57
+ ),
58
+ size: 100,
59
+ }),
60
+ columnHelper.accessor('data', {
61
+ header: 'Data',
62
+ cell: ({ getValue }) => {
63
+ const data = getValue();
64
+ return formatPreviewData(data);
65
+ },
66
+ size: 300,
67
+ }),
68
+ ];
69
+
70
+ export const SSEMessagesTab = ({ selectedRequest }: SSEMessagesTabProps) => {
71
+ // Capture the selected message, so when it gets removed (message limit), it's still displayed
72
+ const [selectedMessage, setSelectedMessage] = useState<SSEMessageRow | null>(
73
+ null
74
+ );
75
+
76
+ const formatData = (data: string) => {
77
+ if (typeof data === 'string') {
78
+ try {
79
+ const jsonData = JSON.parse(data);
80
+ return (
81
+ <div className="bg-gray-800 p-3 rounded border border-gray-700">
82
+ <JsonTree data={jsonData} />
83
+ </div>
84
+ );
85
+ } catch {
86
+ // Fallback to pre tag if JSON parsing fails
87
+ return (
88
+ <pre className="text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto">
89
+ {data}
90
+ </pre>
91
+ );
92
+ }
93
+ }
94
+
95
+ return 'Invalid data';
96
+ };
97
+
98
+ const tableData = useMemo(() => {
99
+ return selectedRequest.messages.map(
100
+ (message): SSEMessageRow => ({
101
+ id: message.id,
102
+ type: message.type,
103
+ data: message.data,
104
+ timestamp: message.timestamp,
105
+ })
106
+ );
107
+ }, [selectedRequest.messages]);
108
+
109
+ const table = useReactTable({
110
+ data: tableData,
111
+ columns,
112
+ getCoreRowModel: getCoreRowModel(),
113
+ });
114
+
115
+ if (selectedRequest.messages.length === 0) {
116
+ return (
117
+ <ScrollArea className="h-full min-h-0 p-4">
118
+ <div className="text-sm text-gray-400">
119
+ No SSE messages available for this connection. Messages will appear
120
+ here when the SSE connection receives data.
121
+ </div>
122
+ </ScrollArea>
123
+ );
124
+ }
125
+
126
+ return (
127
+ <div className="h-full flex flex-col">
128
+ {/* Messages Table */}
129
+ <div className="flex-1 border border-gray-700 rounded overflow-hidden">
130
+ <div className="overflow-y-auto h-full">
131
+ <table className="w-full text-sm">
132
+ <thead className="bg-gray-800 border-b border-gray-700 sticky top-0 z-10">
133
+ {table.getHeaderGroups().map((headerGroup) => (
134
+ <tr key={headerGroup.id}>
135
+ {headerGroup.headers.map((header) => (
136
+ <th
137
+ key={header.id}
138
+ className="text-left p-2 font-medium text-gray-300"
139
+ style={{ width: header.getSize() }}
140
+ >
141
+ <div className="flex items-center gap-1">
142
+ {header.isPlaceholder
143
+ ? null
144
+ : flexRender(
145
+ header.column.columnDef.header,
146
+ header.getContext()
147
+ )}
148
+ </div>
149
+ </th>
150
+ ))}
151
+ </tr>
152
+ ))}
153
+ </thead>
154
+ <tbody>
155
+ {table.getRowModel().rows.map((row) => (
156
+ <tr
157
+ key={row.id}
158
+ className={`border-b border-gray-700 hover:bg-gray-800 cursor-pointer ${
159
+ selectedMessage?.id === row.original.id ? 'bg-gray-800' : ''
160
+ }`}
161
+ onClick={() => setSelectedMessage(row.original)}
162
+ >
163
+ {row.getVisibleCells().map((cell) => (
164
+ <td
165
+ key={cell.id}
166
+ className="p-2"
167
+ style={{ width: cell.column.getSize() }}
168
+ >
169
+ {flexRender(
170
+ cell.column.columnDef.cell,
171
+ cell.getContext()
172
+ )}
173
+ </td>
174
+ ))}
175
+ </tr>
176
+ ))}
177
+ </tbody>
178
+ </table>
179
+ </div>
180
+ </div>
181
+
182
+ {/* Message Details Panel */}
183
+ {selectedMessage && (
184
+ <div className="border-t border-gray-700 bg-gray-800">
185
+ <div className="p-4">
186
+ <div className="flex items-center justify-between mb-3">
187
+ <h4 className="text-sm font-medium text-gray-300">
188
+ Message Details
189
+ </h4>
190
+ <button
191
+ onClick={() => setSelectedMessage(null)}
192
+ className="text-gray-400 hover:text-blue-400 text-sm"
193
+ >
194
+ Close
195
+ </button>
196
+ </div>
197
+ <div className="space-y-3">
198
+ <div className="grid grid-cols-2 gap-4 text-sm">
199
+ <div>
200
+ <span className="text-gray-400">Type: </span>
201
+ <span className="text-purple-400">
202
+ {selectedMessage.type}
203
+ </span>
204
+ </div>
205
+ <div>
206
+ <span className="text-gray-400">Timestamp: </span>
207
+ <span className="text-gray-300">
208
+ {formatTimestamp(selectedMessage.timestamp)}
209
+ </span>
210
+ </div>
211
+ </div>
212
+ <div>
213
+ <span className="text-gray-400 text-sm">Content:</span>
214
+ <div className="mt-2 max-h-96 overflow-y-auto">
215
+ {formatData(selectedMessage.data)}
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ )}
222
+ </div>
223
+ );
224
+ };
@@ -1,32 +1,17 @@
1
1
  import { ScrollArea } from '../components/ScrollArea';
2
- import { NetworkRequest } from '../components/RequestList';
3
- import { NetworkEntry } from '../types';
2
+ import { HttpNetworkEntry } from '../state/model';
4
3
 
5
4
  export type TimingTabProps = {
6
- selectedRequest: NetworkRequest | null;
7
- networkEntries: Map<string, NetworkEntry>;
5
+ selectedRequest: HttpNetworkEntry;
8
6
  };
9
7
 
10
- export const TimingTab = ({
11
- selectedRequest,
12
- networkEntries,
13
- }: TimingTabProps) => {
14
- const networkEntry = selectedRequest
15
- ? networkEntries.get(selectedRequest.id)
8
+ export const TimingTab = ({ selectedRequest }: TimingTabProps) => {
9
+ const startTime = selectedRequest.timestamp || 0;
10
+ const endTime = selectedRequest.duration
11
+ ? selectedRequest.timestamp + selectedRequest.duration
16
12
  : null;
17
-
18
- if (!selectedRequest || !networkEntry) {
19
- return (
20
- <div className="flex items-center justify-center h-full text-gray-400">
21
- Select a request to view timing information
22
- </div>
23
- );
24
- }
25
-
26
- const startTime = networkEntry.startTime || 0;
27
- const endTime = networkEntry.endTime || 0;
28
- const ttfb = networkEntry.ttfb || 0;
29
- const duration = networkEntry.duration || 0;
13
+ const ttfb = selectedRequest.ttfb || 0;
14
+ const duration = selectedRequest.duration || 0;
30
15
 
31
16
  const formatTime = (time: number): string => {
32
17
  if (time < 1) {
@@ -43,31 +28,33 @@ export const TimingTab = ({
43
28
  <ScrollArea className="h-full w-full">
44
29
  <div className="p-4">
45
30
  <div className="space-y-4">
46
- <div className="space-y-2">
47
- <div className="flex justify-between text-sm">
48
- <span className="text-gray-400">Start Time</span>
49
- <span className="text-gray-300">{formatTimestamp(startTime)}</span>
50
- </div>
51
- <div className="flex justify-between text-sm">
52
- <span className="text-gray-400">Time To First Byte (TTFB)</span>
53
- <span className="text-gray-300">{formatTime(ttfb)}</span>
31
+ <div className="space-y-2">
32
+ <div className="flex justify-between text-sm">
33
+ <span className="text-gray-400">Start Time</span>
34
+ <span className="text-gray-300">
35
+ {formatTimestamp(startTime)}
36
+ </span>
37
+ </div>
38
+ <div className="flex justify-between text-sm">
39
+ <span className="text-gray-400">Time To First Byte (TTFB)</span>
40
+ <span className="text-gray-300">{formatTime(ttfb)}</span>
41
+ </div>
42
+ <div className="flex justify-between text-sm">
43
+ <span className="text-gray-400">End Time</span>
44
+ <span className="text-gray-300">
45
+ {endTime ? formatTimestamp(endTime) : 'Pending'}
46
+ </span>
47
+ </div>
54
48
  </div>
55
- <div className="flex justify-between text-sm">
56
- <span className="text-gray-400">End Time</span>
57
- <span className="text-gray-300">
58
- {endTime ? formatTimestamp(endTime) : 'Pending'}
59
- </span>
60
- </div>
61
- </div>
62
49
 
63
- <div className="border-t border-gray-700 pt-4">
64
- <div className="flex justify-between text-sm font-medium">
65
- <span className="text-gray-300">Total Duration</span>
66
- <span className="text-gray-100">{formatTime(duration)}</span>
50
+ <div className="border-t border-gray-700 pt-4">
51
+ <div className="flex justify-between text-sm font-medium">
52
+ <span className="text-gray-300">Total Duration</span>
53
+ <span className="text-gray-100">{formatTime(duration)}</span>
54
+ </div>
67
55
  </div>
68
56
  </div>
69
57
  </div>
70
- </div>
71
58
  </ScrollArea>
72
59
  );
73
60
  };
package/src/ui/types.ts CHANGED
@@ -12,7 +12,10 @@ export type NetworkEntry = {
12
12
  url: string;
13
13
  method: string;
14
14
  headers: HttpHeaders;
15
- postData?: string;
15
+ body?: {
16
+ type: string;
17
+ data: string;
18
+ };
16
19
  status: 'pending' | 'loading' | 'finished' | 'failed';
17
20
  startTime: number;
18
21
  endTime?: number;
@@ -0,0 +1,5 @@
1
+ export function assert(condition: boolean, message: string): asserts condition {
2
+ if (!condition) {
3
+ throw new Error(message);
4
+ }
5
+ }
@@ -0,0 +1,7 @@
1
+ import { NetworkEntry } from '../state/model';
2
+
3
+ export const checkRequestBodyBinary = (request: NetworkEntry) => {
4
+ return (
5
+ request.type === 'http' && request.request.body?.data.type === 'binary'
6
+ );
7
+ };
@@ -0,0 +1,3 @@
1
+ export async function copyToClipboard(text: string) {
2
+ return navigator.clipboard.writeText(text);
3
+ }
@@ -0,0 +1,12 @@
1
+ // Escapes special characters in shell arguments
2
+ export function escapeShellArg(arg: string): string {
3
+ if (!arg) return "''";
4
+
5
+ // If the argument contains no special characters, return as is
6
+ if (/^[a-zA-Z0-9_./:-]+$/.test(arg)) {
7
+ return arg;
8
+ }
9
+
10
+ // Replace single quotes with '\'' and wrap in single quotes
11
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
12
+ }
@@ -0,0 +1,83 @@
1
+ import { HttpHeaders, RequestPostData } from '../../shared/client';
2
+ import { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
3
+ import { escapeShellArg } from './escapeShellArg';
4
+
5
+ const BASE_TAB_INDENT = 2; // Number of spaces for indentation
6
+
7
+ function stringifyData(postData: unknown): string {
8
+ try {
9
+ const jsonString = JSON.stringify(
10
+ typeof postData === 'string' ? JSON.parse(postData) : postData,
11
+ null,
12
+ BASE_TAB_INDENT * 4
13
+ );
14
+
15
+ return jsonString.replace(/([}\]])$/, '$1'.padStart(BASE_TAB_INDENT * 2));
16
+ } catch {
17
+ return String(postData);
18
+ }
19
+ }
20
+
21
+ // Adds a curl parameter with proper indentation
22
+ function addCurlParam(curlParts: string[], flag: string, value: string): void {
23
+ curlParts.push(`${flag.padStart(BASE_TAB_INDENT + flag.length)} ${value}`);
24
+ }
25
+
26
+ function addHttpMethodToCurl(curlParts: string[], method: string): void {
27
+ if (method && hasRequestBody(method)) {
28
+ addCurlParam(curlParts, '-X', method.toUpperCase());
29
+ }
30
+ }
31
+
32
+ function addHeadersToCurl(curlParts: string[], headers: HttpHeaders): void {
33
+ Object.entries(headers).forEach(([key, value]) => {
34
+ addCurlParam(curlParts, '-H', escapeShellArg(`${key}: ${value}`));
35
+ });
36
+ }
37
+
38
+ function hasRequestBody(method: string): boolean {
39
+ const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
40
+
41
+ return methodsWithBody.includes(method.toUpperCase());
42
+ }
43
+
44
+ function addBodyToCurl(curlParts: string[], postData: RequestPostData): void {
45
+ if (!postData) {
46
+ return;
47
+ }
48
+
49
+ const { type, value } = postData;
50
+
51
+ if (type === 'form-data') {
52
+ const formParts = Object.entries(value).map(
53
+ ([key, value]) => `${key}=${stringifyData(value)}`
54
+ );
55
+
56
+ formParts.forEach((part) =>
57
+ addCurlParam(curlParts, '--form', escapeShellArg(part))
58
+ );
59
+
60
+ return;
61
+ }
62
+
63
+ addCurlParam(curlParts, '--data-raw', escapeShellArg(stringifyData(value)));
64
+ }
65
+
66
+ export function generateCurlCommand(
67
+ request: HttpNetworkEntry | SSENetworkEntry
68
+ ) {
69
+ const { method, url, headers = {}, body } = request.request;
70
+
71
+ const postData = body?.data;
72
+
73
+ const curlParts: string[] = [`curl ${escapeShellArg(url)}`];
74
+
75
+ addHttpMethodToCurl(curlParts, method);
76
+ addHeadersToCurl(curlParts, headers);
77
+
78
+ if (postData && hasRequestBody(method)) {
79
+ addBodyToCurl(curlParts, postData);
80
+ }
81
+
82
+ return curlParts.join(' \\\n');
83
+ }
@@ -0,0 +1,64 @@
1
+ import { generateMultipartBody } from './generateMultipartBody';
2
+ import {
3
+ HttpNetworkEntry,
4
+ HttpRequestData,
5
+ SSENetworkEntry,
6
+ } from '../state/model';
7
+ import { getHttpHeaderValueAsString } from '../../utils/getHttpHeaderValueAsString';
8
+ import { HttpHeaders, XHRHeaders } from '../../shared/client';
9
+
10
+ const processHeaders = (requestHeaders: HttpHeaders | undefined) => {
11
+ const headers: XHRHeaders = {};
12
+
13
+ if (!requestHeaders) {
14
+ return headers;
15
+ }
16
+
17
+ Object.entries(requestHeaders).forEach(([name, value]) => {
18
+ // Filter out HTTP/2 pseudo-headers
19
+ if (!name.startsWith(':')) {
20
+ headers[name] = getHttpHeaderValueAsString(value);
21
+ }
22
+ });
23
+
24
+ return headers;
25
+ };
26
+
27
+ const processRequestBody = (body: HttpRequestData, headers: XHRHeaders) => {
28
+ const { type, value } = body.data;
29
+
30
+ switch (type) {
31
+ case 'text':
32
+ return value;
33
+
34
+ case 'form-data': {
35
+ const { body, contentType } = generateMultipartBody(value);
36
+
37
+ headers['Content-Type'] = contentType;
38
+
39
+ return body;
40
+ }
41
+
42
+ default:
43
+ return undefined;
44
+ }
45
+ };
46
+
47
+ export const generateFetchCall = (
48
+ request: HttpNetworkEntry | SSENetworkEntry
49
+ ) => {
50
+ const { url, headers: requestHeaders, method, body } = request.request;
51
+
52
+ const headers = processHeaders(requestHeaders);
53
+ const requestBody = body ? processRequestBody(body, headers) : undefined;
54
+
55
+ const fetchOptions: RequestInit = {
56
+ headers: Object.keys(headers).length ? headers : undefined,
57
+ body: requestBody,
58
+ method,
59
+ };
60
+
61
+ const options = JSON.stringify(fetchOptions, null, 2);
62
+
63
+ return `fetch(${JSON.stringify(url)}, ${options});`;
64
+ };
@@ -0,0 +1,19 @@
1
+ export const generateMultipartBody = (formData: Record<string, unknown>) => {
2
+ const boundary = 'FormBoundary' + Math.random().toString(36).substr(2, 16);
3
+
4
+ const parts: string[] = [];
5
+
6
+ Object.entries(formData).forEach(([key, value]) => {
7
+ parts.push(`--${boundary}`);
8
+ parts.push(`Content-Disposition: form-data; name="${key}"`);
9
+ parts.push('');
10
+ parts.push(String(value));
11
+ });
12
+
13
+ parts.push(`--${boundary}--`);
14
+
15
+ return {
16
+ body: parts.join('\r\n'),
17
+ contentType: `multipart/form-data; boundary=${boundary}`,
18
+ };
19
+ };
@@ -0,0 +1,10 @@
1
+ const idMap = new Map<string, number>();
2
+
3
+ export const getId = (namespace: string) => {
4
+ if (!idMap.has(namespace)) {
5
+ idMap.set(namespace, 0);
6
+ }
7
+ const id = idMap.get(namespace) ?? 0;
8
+ idMap.set(namespace, id + 1);
9
+ return `${namespace}-${id}`;
10
+ };
@@ -0,0 +1,15 @@
1
+ export const getStatusColor = (status: number | string): string => {
2
+ if (typeof status === 'string') {
3
+ // Handle WebSocket statuses
4
+ if (status === 'open') return 'text-green-400';
5
+ if (status === 'connecting') return 'text-yellow-400';
6
+ if (status === 'closed' || status === 'error') return 'text-red-400';
7
+ return 'text-gray-400';
8
+ }
9
+
10
+ // Handle HTTP status codes
11
+ if (status >= 200 && status < 300) return 'text-green-400';
12
+ if (status >= 300 && status < 400) return 'text-yellow-400';
13
+ if (status >= 400) return 'text-red-400';
14
+ return 'text-gray-400';
15
+ };