@rozenite/network-activity-plugin 1.0.0-alpha.9 → 1.1.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/README.md +2 -0
- package/dist/App.html +2 -2
- package/dist/assets/{App-DoHQsY5s.css → App-BrSkOkws.css} +223 -2
- package/dist/assets/{App-CA1Fbh0I.js → App-Kyi7zHUX.js} +8188 -2671
- package/dist/react-native.cjs +4 -1
- package/dist/react-native.js +4 -1
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/config.d.ts +20 -0
- package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
- package/dist/src/react-native/http/xhr-interceptor.d.ts +7 -1
- package/dist/src/react-native/sse/sse-interceptor.d.ts +2 -2
- package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
- package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
- package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
- package/dist/src/shared/client.d.ts +55 -4
- package/dist/src/shared/sse-events.d.ts +4 -1
- package/dist/src/ui/components/Button.d.ts +2 -2
- package/dist/src/ui/components/CodeBlock.d.ts +3 -0
- package/dist/src/ui/components/CodeEditor.d.ts +5 -0
- package/dist/src/ui/components/CookieCard.d.ts +7 -0
- package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
- package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
- package/dist/src/ui/components/FilterBar.d.ts +10 -0
- package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +1 -1
- package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
- package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
- package/dist/src/ui/components/RequestBody.d.ts +6 -0
- package/dist/src/ui/components/RequestList.d.ts +9 -4
- package/dist/src/ui/components/ScrollArea.d.ts +3 -2
- package/dist/src/ui/components/Section.d.ts +8 -0
- package/dist/src/ui/components/Separator.d.ts +2 -1
- package/dist/src/ui/components/Tabs.d.ts +7 -0
- package/dist/src/ui/state/hooks.d.ts +4 -0
- package/dist/src/ui/state/model.d.ts +22 -7
- package/dist/src/ui/state/store.d.ts +27 -3
- package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
- package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
- package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
- package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
- package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
- package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
- package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
- package/dist/src/utils/cookieParser.d.ts +6 -0
- package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
- package/dist/src/utils/getHttpHeader.d.ts +5 -0
- package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
- package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
- package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
- package/dist/src/utils/safeStringify.d.ts +1 -0
- package/dist/src/utils/typeChecks.d.ts +9 -0
- package/dist/useNetworkActivityDevTools.cjs +337 -24
- package/dist/useNetworkActivityDevTools.js +338 -25
- package/package.json +7 -4
- package/react-native.ts +6 -1
- package/src/react-native/config.ts +43 -0
- package/src/react-native/http/network-inspector.ts +190 -8
- package/src/react-native/http/overrides-registry.ts +32 -0
- package/src/react-native/http/xhr-interceptor.ts +19 -2
- package/src/react-native/sse/sse-inspector.ts +27 -5
- package/src/react-native/sse/sse-interceptor.ts +26 -8
- package/src/react-native/useNetworkActivityDevTools.ts +86 -8
- package/src/react-native/utils/getBlobName.ts +45 -0
- package/src/react-native/utils/getFormDataEntries.ts +32 -0
- package/src/react-native/utils.ts +3 -3
- package/src/shared/client.ts +81 -4
- package/src/shared/sse-events.ts +4 -1
- package/src/ui/components/Button.tsx +1 -0
- package/src/ui/components/CodeBlock.tsx +19 -0
- package/src/ui/components/CodeEditor.tsx +26 -0
- package/src/ui/components/CookieCard.tsx +64 -0
- package/src/ui/components/CopyRequestDropdown.tsx +95 -0
- package/src/ui/components/DropdownMenu.tsx +206 -0
- package/src/ui/components/FilterBar.tsx +117 -0
- package/src/ui/components/Input.tsx +1 -1
- package/src/ui/components/JsonTree.tsx +10 -3
- package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
- package/src/ui/components/KeyValueGrid.tsx +51 -0
- package/src/ui/components/OverrideResponse.tsx +132 -0
- package/src/ui/components/RequestBody.tsx +86 -0
- package/src/ui/components/RequestList.tsx +74 -14
- package/src/ui/components/ScrollArea.tsx +1 -0
- package/src/ui/components/Section.tsx +46 -0
- package/src/ui/components/SidePanel.tsx +15 -5
- package/src/ui/components/Toolbar.tsx +3 -2
- package/src/ui/globals.css +4 -0
- package/src/ui/hooks/useCopyToClipboard.ts +2 -2
- package/src/ui/state/derived.ts +2 -0
- package/src/ui/state/hooks.ts +8 -0
- package/src/ui/state/model.ts +28 -7
- package/src/ui/state/store.ts +640 -500
- package/src/ui/tabs/CookiesTab.tsx +60 -263
- package/src/ui/tabs/HeadersTab.tsx +78 -89
- package/src/ui/tabs/RequestTab.tsx +58 -46
- package/src/ui/tabs/ResponseTab.tsx +98 -67
- package/src/ui/tabs/SSEMessagesTab.tsx +50 -39
- package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
- package/src/ui/utils/escapeShellArg.ts +12 -0
- package/src/ui/utils/generateCurlCommand.ts +83 -0
- package/src/ui/utils/generateFetchCall.ts +64 -0
- package/src/ui/utils/generateMultipartBody.ts +19 -0
- package/src/ui/views/InspectorView.tsx +15 -3
- package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
- package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
- package/src/utils/cookieParser.ts +126 -0
- package/src/utils/getContentTypeMimeType.ts +17 -0
- package/src/utils/getHttpHeader.ts +17 -0
- package/src/utils/getHttpHeaderValueAsString.ts +13 -0
- package/src/utils/getStringSizeInBytes.ts +3 -0
- package/src/utils/inferContentTypeFromPostData.ts +9 -0
- package/src/utils/safeStringify.ts +7 -0
- package/src/utils/typeChecks.ts +27 -0
- package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
- package/src/ui/utils/getHttpHeaderValue.ts +0 -14
|
@@ -1,18 +1,49 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { ScrollArea } from '../components/ScrollArea';
|
|
3
3
|
import { JsonTree } from '../components/JsonTree';
|
|
4
4
|
import { HttpNetworkEntry } from '../state/model';
|
|
5
|
+
import { Section } from '../components/Section';
|
|
6
|
+
import { KeyValueGrid } from '../components/KeyValueGrid';
|
|
7
|
+
import { CodeBlock } from '../components/CodeBlock';
|
|
8
|
+
import { useOverrides } from '../state/hooks';
|
|
9
|
+
import { RequestOverride } from '../../shared/client';
|
|
10
|
+
import { OverrideResponse } from '../components/OverrideResponse';
|
|
11
|
+
import { Button } from '../components/Button';
|
|
12
|
+
import { Pencil } from 'lucide-react';
|
|
5
13
|
|
|
6
14
|
export type ResponseTabProps = {
|
|
7
15
|
selectedRequest: HttpNetworkEntry;
|
|
8
16
|
onRequestResponseBody: (requestId: string) => void;
|
|
9
17
|
};
|
|
10
18
|
|
|
19
|
+
type ResponseBodySectionProps = {
|
|
20
|
+
action?: React.ReactNode;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const RenderResponseBodySection = ({
|
|
25
|
+
children,
|
|
26
|
+
action,
|
|
27
|
+
}: ResponseBodySectionProps) => {
|
|
28
|
+
return (
|
|
29
|
+
<Section title="Response Body" collapsible={false} action={action}>
|
|
30
|
+
<div className="space-y-4">{children}</div>
|
|
31
|
+
</Section>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
11
35
|
export const ResponseTab = ({
|
|
12
36
|
selectedRequest,
|
|
13
37
|
onRequestResponseBody,
|
|
14
38
|
}: ResponseTabProps) => {
|
|
15
39
|
const onRequestResponseBodyRef = useRef(onRequestResponseBody);
|
|
40
|
+
const overrides = useOverrides();
|
|
41
|
+
const [initialOverride, setInitialOverride] = useState<
|
|
42
|
+
RequestOverride | undefined
|
|
43
|
+
>(() => {
|
|
44
|
+
const override = overrides.get(selectedRequest.request.url);
|
|
45
|
+
return override;
|
|
46
|
+
});
|
|
16
47
|
|
|
17
48
|
useEffect(() => {
|
|
18
49
|
onRequestResponseBodyRef.current = onRequestResponseBody;
|
|
@@ -24,10 +55,10 @@ export const ResponseTab = ({
|
|
|
24
55
|
}
|
|
25
56
|
}, [selectedRequest.id]);
|
|
26
57
|
|
|
27
|
-
const
|
|
28
|
-
const responseBody = selectedRequest.response?.body;
|
|
58
|
+
const responseBody = selectedRequest.response?.body;
|
|
29
59
|
|
|
30
|
-
|
|
60
|
+
const renderResponseBody = () => {
|
|
61
|
+
if (!responseBody || responseBody.data === null) {
|
|
31
62
|
return (
|
|
32
63
|
<div className="text-sm text-gray-400">
|
|
33
64
|
No response body available for this request
|
|
@@ -36,103 +67,103 @@ export const ResponseTab = ({
|
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
const { type, data } = responseBody;
|
|
70
|
+
const statusCode = selectedRequest.response?.status;
|
|
71
|
+
|
|
72
|
+
const contentTypeGrid = (
|
|
73
|
+
<KeyValueGrid
|
|
74
|
+
items={[
|
|
75
|
+
{
|
|
76
|
+
key: 'Content-Type',
|
|
77
|
+
value: type,
|
|
78
|
+
valueClassName: 'text-blue-400',
|
|
79
|
+
},
|
|
80
|
+
]}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const overrideAction = (
|
|
85
|
+
<Button
|
|
86
|
+
variant="ghost"
|
|
87
|
+
size="xs"
|
|
88
|
+
className="text-violet-300 hover:text-violet-300"
|
|
89
|
+
onClick={() =>
|
|
90
|
+
setInitialOverride({
|
|
91
|
+
body: data,
|
|
92
|
+
status: statusCode,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
<Pencil className="h-2 w-2" />
|
|
97
|
+
Override
|
|
98
|
+
</Button>
|
|
99
|
+
);
|
|
39
100
|
|
|
40
|
-
|
|
41
|
-
if (data === null) {
|
|
101
|
+
if (initialOverride !== undefined) {
|
|
42
102
|
return (
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
103
|
+
<OverrideResponse
|
|
104
|
+
selectedRequest={selectedRequest}
|
|
105
|
+
initialOverride={initialOverride}
|
|
106
|
+
onClear={() => setInitialOverride(undefined)}
|
|
107
|
+
/>
|
|
46
108
|
);
|
|
47
109
|
}
|
|
48
110
|
|
|
49
|
-
|
|
50
|
-
|
|
111
|
+
if (type.startsWith('application/json')) {
|
|
112
|
+
let bodyContent;
|
|
113
|
+
|
|
51
114
|
try {
|
|
52
115
|
const jsonData = JSON.parse(data);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</div>
|
|
59
|
-
<div className="bg-gray-800 p-3 rounded border border-gray-700">
|
|
60
|
-
<JsonTree data={jsonData} />
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
116
|
+
|
|
117
|
+
bodyContent = (
|
|
118
|
+
<CodeBlock>
|
|
119
|
+
<JsonTree data={jsonData} />
|
|
120
|
+
</CodeBlock>
|
|
63
121
|
);
|
|
64
122
|
} catch {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<div className="text-
|
|
69
|
-
<span className="text-gray-400">Content-Type: </span>
|
|
70
|
-
<span className="text-blue-400">{type}</span>
|
|
71
|
-
</div>
|
|
72
|
-
<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">
|
|
73
|
-
{data}
|
|
74
|
-
</pre>
|
|
75
|
-
<div className="text-xs text-gray-500">
|
|
123
|
+
bodyContent = (
|
|
124
|
+
<>
|
|
125
|
+
<CodeBlock>{data}</CodeBlock>
|
|
126
|
+
<div className="text-xs text-gray-500 mt-1">
|
|
76
127
|
⚠️ Failed to parse as JSON, showing as raw text
|
|
77
128
|
</div>
|
|
78
|
-
|
|
129
|
+
</>
|
|
79
130
|
);
|
|
80
131
|
}
|
|
81
|
-
}
|
|
82
132
|
|
|
83
|
-
// Handle HTML content
|
|
84
|
-
if (type === 'text/html') {
|
|
85
133
|
return (
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</div>
|
|
91
|
-
<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">
|
|
92
|
-
{data}
|
|
93
|
-
</pre>
|
|
94
|
-
</div>
|
|
134
|
+
<RenderResponseBodySection action={overrideAction}>
|
|
135
|
+
{contentTypeGrid}
|
|
136
|
+
{bodyContent}
|
|
137
|
+
</RenderResponseBodySection>
|
|
95
138
|
);
|
|
96
139
|
}
|
|
97
140
|
|
|
98
|
-
// Handle other text content types
|
|
99
141
|
if (
|
|
100
142
|
type.startsWith('text/') ||
|
|
101
|
-
type
|
|
102
|
-
type
|
|
143
|
+
type.startsWith('application/xml') ||
|
|
144
|
+
type.startsWith('application/javascript')
|
|
103
145
|
) {
|
|
104
146
|
return (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
</div>
|
|
110
|
-
<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">
|
|
111
|
-
{data}
|
|
112
|
-
</pre>
|
|
113
|
-
</div>
|
|
147
|
+
<RenderResponseBodySection action={overrideAction}>
|
|
148
|
+
{contentTypeGrid}
|
|
149
|
+
<CodeBlock>{data}</CodeBlock>
|
|
150
|
+
</RenderResponseBodySection>
|
|
114
151
|
);
|
|
115
152
|
}
|
|
116
153
|
|
|
117
|
-
// Handle other content types
|
|
118
154
|
return (
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
<span className="text-gray-400">Content-Type: </span>
|
|
122
|
-
<span className="text-blue-400">{type}</span>
|
|
123
|
-
</div>
|
|
155
|
+
<RenderResponseBodySection>
|
|
156
|
+
{contentTypeGrid}
|
|
124
157
|
<div className="text-sm text-gray-400">
|
|
125
158
|
Binary content not shown - {data.length} bytes
|
|
126
159
|
</div>
|
|
127
|
-
</
|
|
160
|
+
</RenderResponseBodySection>
|
|
128
161
|
);
|
|
129
162
|
};
|
|
130
163
|
|
|
131
164
|
return (
|
|
132
165
|
<ScrollArea className="h-full w-full">
|
|
133
|
-
<div className="p-4">
|
|
134
|
-
{renderResponseBody()}
|
|
135
|
-
</div>
|
|
166
|
+
<div className="p-4">{renderResponseBody()}</div>
|
|
136
167
|
</ScrollArea>
|
|
137
168
|
);
|
|
138
169
|
};
|
|
@@ -15,30 +15,64 @@ export type SSEMessagesTabProps = {
|
|
|
15
15
|
|
|
16
16
|
interface SSEMessageRow {
|
|
17
17
|
id: string;
|
|
18
|
+
type: string;
|
|
18
19
|
data: string;
|
|
19
20
|
timestamp: number;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const columnHelper = createColumnHelper<SSEMessageRow>();
|
|
23
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
|
+
|
|
24
70
|
export const SSEMessagesTab = ({ selectedRequest }: SSEMessagesTabProps) => {
|
|
25
71
|
// Capture the selected message, so when it gets removed (message limit), it's still displayed
|
|
26
72
|
const [selectedMessage, setSelectedMessage] = useState<SSEMessageRow | null>(
|
|
27
73
|
null
|
|
28
74
|
);
|
|
29
75
|
|
|
30
|
-
const formatTimestamp = (timestamp: number) => {
|
|
31
|
-
const date = new Date(timestamp);
|
|
32
|
-
const timeString = date.toLocaleTimeString('en-US', {
|
|
33
|
-
hour12: false,
|
|
34
|
-
hour: '2-digit',
|
|
35
|
-
minute: '2-digit',
|
|
36
|
-
second: '2-digit',
|
|
37
|
-
});
|
|
38
|
-
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
|
|
39
|
-
return `${timeString}.${milliseconds}`;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
76
|
const formatData = (data: string) => {
|
|
43
77
|
if (typeof data === 'string') {
|
|
44
78
|
try {
|
|
@@ -65,38 +99,13 @@ export const SSEMessagesTab = ({ selectedRequest }: SSEMessagesTabProps) => {
|
|
|
65
99
|
return selectedRequest.messages.map(
|
|
66
100
|
(message): SSEMessageRow => ({
|
|
67
101
|
id: message.id,
|
|
102
|
+
type: message.type,
|
|
68
103
|
data: message.data,
|
|
69
104
|
timestamp: message.timestamp,
|
|
70
105
|
})
|
|
71
106
|
);
|
|
72
107
|
}, [selectedRequest.messages]);
|
|
73
108
|
|
|
74
|
-
const formatPreviewData = (data: string) => {
|
|
75
|
-
return (
|
|
76
|
-
<span className="max-w-xs truncate text-gray-400">
|
|
77
|
-
{data.substring(0, 100) + (data.length > 100 ? '...' : '')}
|
|
78
|
-
</span>
|
|
79
|
-
);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const columns = [
|
|
83
|
-
columnHelper.accessor('data', {
|
|
84
|
-
header: 'Data',
|
|
85
|
-
cell: ({ getValue }) => {
|
|
86
|
-
const data = getValue();
|
|
87
|
-
return formatPreviewData(data);
|
|
88
|
-
},
|
|
89
|
-
size: 300,
|
|
90
|
-
}),
|
|
91
|
-
columnHelper.accessor('timestamp', {
|
|
92
|
-
header: 'Timestamp',
|
|
93
|
-
cell: ({ getValue }) => (
|
|
94
|
-
<div className="text-gray-400">{formatTimestamp(getValue())}</div>
|
|
95
|
-
),
|
|
96
|
-
size: 120,
|
|
97
|
-
}),
|
|
98
|
-
];
|
|
99
|
-
|
|
100
109
|
const table = useReactTable({
|
|
101
110
|
data: tableData,
|
|
102
111
|
columns,
|
|
@@ -189,7 +198,9 @@ export const SSEMessagesTab = ({ selectedRequest }: SSEMessagesTabProps) => {
|
|
|
189
198
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
190
199
|
<div>
|
|
191
200
|
<span className="text-gray-400">Type: </span>
|
|
192
|
-
<span className="text-purple-400">
|
|
201
|
+
<span className="text-purple-400">
|
|
202
|
+
{selectedMessage.type}
|
|
203
|
+
</span>
|
|
193
204
|
</div>
|
|
194
205
|
<div>
|
|
195
206
|
<span className="text-gray-400">Timestamp: </span>
|
|
@@ -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
|
+
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
2
|
import { Toolbar } from '../components/Toolbar';
|
|
3
3
|
import { RequestList } from '../components/RequestList';
|
|
4
4
|
import { SidePanel } from '../components/SidePanel';
|
|
5
|
+
import { FilterBar, FilterState } from '../components/FilterBar';
|
|
5
6
|
import { NetworkActivityDevToolsClient } from '../../shared/client';
|
|
6
7
|
import {
|
|
7
8
|
useNetworkActivityClientManagement,
|
|
8
9
|
useHasSelectedRequest,
|
|
9
10
|
useNetworkActivityActions,
|
|
11
|
+
useOverrides,
|
|
10
12
|
} from '../state/hooks';
|
|
11
13
|
|
|
12
14
|
export type InspectorViewProps = {
|
|
@@ -17,6 +19,11 @@ export const InspectorView = ({ client }: InspectorViewProps) => {
|
|
|
17
19
|
const actions = useNetworkActivityActions();
|
|
18
20
|
const clientManagement = useNetworkActivityClientManagement();
|
|
19
21
|
const hasSelectedRequest = useHasSelectedRequest();
|
|
22
|
+
const overrides = useOverrides();
|
|
23
|
+
const [filter, setFilter] = useState<FilterState>({
|
|
24
|
+
text: '',
|
|
25
|
+
types: new Set(['http', 'websocket', 'sse']),
|
|
26
|
+
});
|
|
20
27
|
|
|
21
28
|
useEffect(() => {
|
|
22
29
|
if (!client) {
|
|
@@ -26,15 +33,20 @@ export const InspectorView = ({ client }: InspectorViewProps) => {
|
|
|
26
33
|
clientManagement.setupClient(client);
|
|
27
34
|
actions.setRecording(true);
|
|
28
35
|
|
|
36
|
+
client.send('set-overrides', {
|
|
37
|
+
overrides: Array.from(overrides.entries()),
|
|
38
|
+
});
|
|
39
|
+
|
|
29
40
|
return () => {
|
|
30
41
|
actions.setRecording(false);
|
|
31
42
|
clientManagement.cleanupClient();
|
|
32
43
|
};
|
|
33
|
-
}, [client, clientManagement, actions]);
|
|
44
|
+
}, [client, clientManagement, actions, overrides]);
|
|
34
45
|
|
|
35
46
|
return (
|
|
36
47
|
<div className="h-screen bg-gray-900 text-gray-100 flex flex-col">
|
|
37
48
|
<Toolbar />
|
|
49
|
+
<FilterBar filter={filter} onFilterChange={setFilter} />
|
|
38
50
|
|
|
39
51
|
<div className="flex flex-1 overflow-hidden">
|
|
40
52
|
{/* Request List */}
|
|
@@ -43,7 +55,7 @@ export const InspectorView = ({ client }: InspectorViewProps) => {
|
|
|
43
55
|
hasSelectedRequest ? 'w-1/2' : 'w-full'
|
|
44
56
|
} border-r border-gray-700 overflow-hidden`}
|
|
45
57
|
>
|
|
46
|
-
<RequestList />
|
|
58
|
+
<RequestList filter={filter} />
|
|
47
59
|
</div>
|
|
48
60
|
|
|
49
61
|
{hasSelectedRequest && <SidePanel />}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HttpHeaders, RequestPostData } from '../shared/client';
|
|
2
|
+
import { getHttpHeader } from './getHttpHeader';
|
|
3
|
+
import { inferContentTypeFromPostData } from './inferContentTypeFromPostData';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Partially emulates React Native's behavior for setting HTTP headers.
|
|
7
|
+
*
|
|
8
|
+
* @see https://github.com/facebook/react-native/blob/de5093c88771977b58f7bec3f3ffa64a9595334e/packages/react-native/Libraries/Network/RCTNetworking.mm#L345-L349
|
|
9
|
+
*/
|
|
10
|
+
export function applyReactNativeRequestHeadersLogic(
|
|
11
|
+
headers: HttpHeaders,
|
|
12
|
+
postData?: RequestPostData
|
|
13
|
+
): HttpHeaders {
|
|
14
|
+
const existingContentType = getHttpHeader(headers, 'content-type');
|
|
15
|
+
|
|
16
|
+
if (existingContentType) {
|
|
17
|
+
return headers;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const inferredContentType = inferContentTypeFromPostData(postData);
|
|
21
|
+
|
|
22
|
+
if (!inferredContentType) {
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...headers,
|
|
28
|
+
'Content-Type': inferredContentType,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HttpHeaders, XHRHeaders } from '../shared/client';
|
|
2
|
+
import { splitSetCookieHeaderByComma } from './cookieParser';
|
|
3
|
+
import { getHttpHeader } from './getHttpHeader';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Applies React Native specific logic to response headers.
|
|
7
|
+
* React Native concatenates multiple header values into single strings,
|
|
8
|
+
* this function parses them back into arrays where appropriate.
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/facebook/react-native/blob/588f0c5ce6c283f116228456da2170d2adc3cbf4/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java#L637
|
|
11
|
+
*/
|
|
12
|
+
export const applyReactNativeResponseHeadersLogic = (
|
|
13
|
+
headers: XHRHeaders
|
|
14
|
+
): HttpHeaders => {
|
|
15
|
+
const parsedHeaders: HttpHeaders = { ...headers };
|
|
16
|
+
|
|
17
|
+
const setCookieHeader = getHttpHeader(headers, 'set-cookie');
|
|
18
|
+
|
|
19
|
+
if (setCookieHeader) {
|
|
20
|
+
const { value, originalKey } = setCookieHeader;
|
|
21
|
+
|
|
22
|
+
const cookies = splitSetCookieHeaderByComma(value);
|
|
23
|
+
|
|
24
|
+
parsedHeaders[originalKey] = cookies.length > 0 ? cookies : value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return parsedHeaders;
|
|
28
|
+
};
|