network-terminal 1.0.2 → 1.0.3
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/bin/cli.js +40 -0
- package/package.json +19 -6
- package/src/components/LogEntry.tsx +121 -0
- package/src/components/NetworkTerminal.tsx +219 -0
- package/src/components/Terminal.tsx +86 -0
- package/src/components/TerminalHeader.tsx +93 -0
- package/src/hooks/useNetworkInterceptor.ts +190 -0
- package/src/index.ts +17 -0
- package/src/types.ts +50 -0
- package/src/utils/colors.ts +18 -0
- package/src/utils/formatters.ts +26 -0
- package/src/vite-plugin.ts +88 -0
- package/standalone/index.html +25 -0
- package/standalone/main.tsx +216 -0
- package/standalone/vite.config.js +12 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { createServer } = require('vite');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const portIndex = args.indexOf('--port');
|
|
8
|
+
const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3000;
|
|
9
|
+
|
|
10
|
+
async function start() {
|
|
11
|
+
const standalonePath = path.join(__dirname, '..', 'standalone');
|
|
12
|
+
|
|
13
|
+
console.log('\x1b[32m%s\x1b[0m', '\n >_ Network Terminal\n');
|
|
14
|
+
console.log(' Starting development server...\n');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const server = await createServer({
|
|
18
|
+
configFile: path.join(standalonePath, 'vite.config.js'),
|
|
19
|
+
root: standalonePath,
|
|
20
|
+
server: {
|
|
21
|
+
port,
|
|
22
|
+
open: true,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await server.listen();
|
|
27
|
+
|
|
28
|
+
const actualPort = server.config.server.port;
|
|
29
|
+
|
|
30
|
+
console.log('\x1b[32m%s\x1b[0m', ` Server running at:\n`);
|
|
31
|
+
console.log(` > Local: \x1b[36mhttp://localhost:${actualPort}\x1b[0m`);
|
|
32
|
+
console.log('\n Press \x1b[33mCtrl+C\x1b[0m to stop\n');
|
|
33
|
+
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('\x1b[31mError starting server:\x1b[0m', err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
start();
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "network-terminal",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A browser-based terminal UI for monitoring Fetch/XHR requests with real-time display of routes, payloads, and responses",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"network-terminal": "./bin/cli.js"
|
|
10
|
+
},
|
|
8
11
|
"exports": {
|
|
9
12
|
".": {
|
|
10
13
|
"types": "./dist/index.d.ts",
|
|
@@ -18,7 +21,10 @@
|
|
|
18
21
|
}
|
|
19
22
|
},
|
|
20
23
|
"files": [
|
|
21
|
-
"dist"
|
|
24
|
+
"dist",
|
|
25
|
+
"bin",
|
|
26
|
+
"standalone",
|
|
27
|
+
"src"
|
|
22
28
|
],
|
|
23
29
|
"scripts": {
|
|
24
30
|
"build": "tsup",
|
|
@@ -40,18 +46,25 @@
|
|
|
40
46
|
],
|
|
41
47
|
"author": "",
|
|
42
48
|
"license": "MIT",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
51
|
+
"react": "^18.2.0",
|
|
52
|
+
"react-dom": "^18.2.0",
|
|
53
|
+
"vite": "^5.0.0"
|
|
54
|
+
},
|
|
43
55
|
"peerDependencies": {
|
|
44
56
|
"react": ">=17.0.0",
|
|
45
57
|
"react-dom": ">=17.0.0"
|
|
46
58
|
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"react": { "optional": true },
|
|
61
|
+
"react-dom": { "optional": true }
|
|
62
|
+
},
|
|
47
63
|
"devDependencies": {
|
|
48
64
|
"@types/react": "^18.2.0",
|
|
49
65
|
"@types/react-dom": "^18.2.0",
|
|
50
|
-
"react": "^18.2.0",
|
|
51
|
-
"react-dom": "^18.2.0",
|
|
52
66
|
"tsup": "^8.0.0",
|
|
53
|
-
"typescript": "^5.0.0"
|
|
54
|
-
"vite": "^7.3.1"
|
|
67
|
+
"typescript": "^5.0.0"
|
|
55
68
|
},
|
|
56
69
|
"repository": {
|
|
57
70
|
"type": "git",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LogEntryProps } from '../types';
|
|
3
|
+
import { formatJson, formatTime } from '../utils/formatters';
|
|
4
|
+
import { getStatusColor, getMethodColor } from '../utils/colors';
|
|
5
|
+
|
|
6
|
+
const styles = {
|
|
7
|
+
container: {
|
|
8
|
+
marginBottom: '16px',
|
|
9
|
+
borderBottom: '1px solid #1f2937',
|
|
10
|
+
paddingBottom: '16px',
|
|
11
|
+
},
|
|
12
|
+
header: {
|
|
13
|
+
display: 'flex',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
gap: '8px',
|
|
16
|
+
marginBottom: '4px',
|
|
17
|
+
flexWrap: 'wrap' as const,
|
|
18
|
+
},
|
|
19
|
+
timestamp: {
|
|
20
|
+
color: '#6b7280',
|
|
21
|
+
fontSize: '12px',
|
|
22
|
+
fontFamily: 'monospace',
|
|
23
|
+
},
|
|
24
|
+
method: {
|
|
25
|
+
fontWeight: 'bold',
|
|
26
|
+
fontFamily: 'monospace',
|
|
27
|
+
},
|
|
28
|
+
status: {
|
|
29
|
+
fontWeight: 'bold',
|
|
30
|
+
fontFamily: 'monospace',
|
|
31
|
+
},
|
|
32
|
+
duration: {
|
|
33
|
+
color: '#6b7280',
|
|
34
|
+
fontSize: '12px',
|
|
35
|
+
fontFamily: 'monospace',
|
|
36
|
+
},
|
|
37
|
+
url: {
|
|
38
|
+
color: '#60a5fa',
|
|
39
|
+
wordBreak: 'break-all' as const,
|
|
40
|
+
marginBottom: '8px',
|
|
41
|
+
fontFamily: 'monospace',
|
|
42
|
+
fontSize: '13px',
|
|
43
|
+
},
|
|
44
|
+
bodyContainer: {
|
|
45
|
+
marginTop: '8px',
|
|
46
|
+
},
|
|
47
|
+
bodyLabel: {
|
|
48
|
+
color: '#a78bfa',
|
|
49
|
+
fontSize: '12px',
|
|
50
|
+
marginBottom: '4px',
|
|
51
|
+
fontFamily: 'monospace',
|
|
52
|
+
},
|
|
53
|
+
pre: {
|
|
54
|
+
backgroundColor: '#1f2937',
|
|
55
|
+
padding: '8px',
|
|
56
|
+
borderRadius: '4px',
|
|
57
|
+
color: '#86efac',
|
|
58
|
+
overflowX: 'auto' as const,
|
|
59
|
+
fontSize: '12px',
|
|
60
|
+
whiteSpace: 'pre-wrap' as const,
|
|
61
|
+
fontFamily: 'monospace',
|
|
62
|
+
maxHeight: '192px',
|
|
63
|
+
overflowY: 'auto' as const,
|
|
64
|
+
margin: 0,
|
|
65
|
+
},
|
|
66
|
+
errorPre: {
|
|
67
|
+
backgroundColor: '#1f2937',
|
|
68
|
+
padding: '8px',
|
|
69
|
+
borderRadius: '4px',
|
|
70
|
+
color: '#fca5a5',
|
|
71
|
+
overflowX: 'auto' as const,
|
|
72
|
+
fontSize: '12px',
|
|
73
|
+
whiteSpace: 'pre-wrap' as const,
|
|
74
|
+
fontFamily: 'monospace',
|
|
75
|
+
margin: 0,
|
|
76
|
+
},
|
|
77
|
+
} as const;
|
|
78
|
+
|
|
79
|
+
export const LogEntry: React.FC<LogEntryProps> = ({ log, type }) => {
|
|
80
|
+
return (
|
|
81
|
+
<div style={styles.container}>
|
|
82
|
+
<div style={styles.header}>
|
|
83
|
+
<span style={styles.timestamp}>[{formatTime(log.timestamp)}]</span>
|
|
84
|
+
<span style={{ ...styles.method, color: getMethodColor(log.method) }}>
|
|
85
|
+
{log.method}
|
|
86
|
+
</span>
|
|
87
|
+
{type === 'response' && log.status && (
|
|
88
|
+
<span style={{ ...styles.status, color: getStatusColor(log.status) }}>
|
|
89
|
+
{log.status} {log.statusText}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
{log.duration !== undefined && type === 'response' && (
|
|
93
|
+
<span style={styles.duration}>({log.duration}ms)</span>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div style={styles.url}>{log.url}</div>
|
|
98
|
+
|
|
99
|
+
{type === 'request' ? (
|
|
100
|
+
log.requestBody ? (
|
|
101
|
+
<div style={styles.bodyContainer}>
|
|
102
|
+
<div style={styles.bodyLabel}>{'\u25B8'} Payload:</div>
|
|
103
|
+
<pre style={styles.pre}>{formatJson(log.requestBody)}</pre>
|
|
104
|
+
</div>
|
|
105
|
+
) : null
|
|
106
|
+
) : log.error ? (
|
|
107
|
+
<div style={styles.bodyContainer}>
|
|
108
|
+
<div style={{ ...styles.bodyLabel, color: '#f87171' }}>
|
|
109
|
+
{'\u25B8'} Error:
|
|
110
|
+
</div>
|
|
111
|
+
<pre style={styles.errorPre}>{log.error}</pre>
|
|
112
|
+
</div>
|
|
113
|
+
) : log.responseBody ? (
|
|
114
|
+
<div style={styles.bodyContainer}>
|
|
115
|
+
<div style={styles.bodyLabel}>{'\u25B8'} Response:</div>
|
|
116
|
+
<pre style={styles.pre}>{formatJson(log.responseBody)}</pre>
|
|
117
|
+
</div>
|
|
118
|
+
) : null}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { NetworkLog, NetworkTerminalProps } from '../types';
|
|
3
|
+
import { Terminal } from './Terminal';
|
|
4
|
+
import { useNetworkInterceptor } from '../hooks/useNetworkInterceptor';
|
|
5
|
+
|
|
6
|
+
const styles = {
|
|
7
|
+
floatingButton: {
|
|
8
|
+
position: 'fixed' as const,
|
|
9
|
+
bottom: '16px',
|
|
10
|
+
right: '16px',
|
|
11
|
+
zIndex: 9999,
|
|
12
|
+
backgroundColor: '#1f2937',
|
|
13
|
+
color: '#4ade80',
|
|
14
|
+
padding: '8px 16px',
|
|
15
|
+
borderRadius: '8px',
|
|
16
|
+
fontFamily: 'monospace',
|
|
17
|
+
fontSize: '14px',
|
|
18
|
+
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
|
19
|
+
border: '1px solid #374151',
|
|
20
|
+
cursor: 'pointer',
|
|
21
|
+
},
|
|
22
|
+
container: {
|
|
23
|
+
position: 'fixed' as const,
|
|
24
|
+
bottom: 0,
|
|
25
|
+
left: 0,
|
|
26
|
+
right: 0,
|
|
27
|
+
zIndex: 9999,
|
|
28
|
+
backgroundColor: '#111827',
|
|
29
|
+
borderTop: '2px solid #22c55e',
|
|
30
|
+
boxShadow: '0 -10px 15px -3px rgba(0, 0, 0, 0.1)',
|
|
31
|
+
},
|
|
32
|
+
containerTop: {
|
|
33
|
+
position: 'fixed' as const,
|
|
34
|
+
top: 0,
|
|
35
|
+
left: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
zIndex: 9999,
|
|
38
|
+
backgroundColor: '#111827',
|
|
39
|
+
borderBottom: '2px solid #22c55e',
|
|
40
|
+
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
|
41
|
+
},
|
|
42
|
+
mainHeader: {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
justifyContent: 'space-between',
|
|
46
|
+
padding: '8px 16px',
|
|
47
|
+
backgroundColor: '#1f2937',
|
|
48
|
+
borderBottom: '1px solid #374151',
|
|
49
|
+
},
|
|
50
|
+
headerLeft: {
|
|
51
|
+
display: 'flex',
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
gap: '12px',
|
|
54
|
+
},
|
|
55
|
+
title: {
|
|
56
|
+
color: '#4ade80',
|
|
57
|
+
fontFamily: 'monospace',
|
|
58
|
+
fontWeight: 'bold',
|
|
59
|
+
},
|
|
60
|
+
hint: {
|
|
61
|
+
color: '#6b7280',
|
|
62
|
+
fontFamily: 'monospace',
|
|
63
|
+
fontSize: '12px',
|
|
64
|
+
},
|
|
65
|
+
headerRight: {
|
|
66
|
+
display: 'flex',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
gap: '8px',
|
|
69
|
+
},
|
|
70
|
+
clearButton: {
|
|
71
|
+
color: '#9ca3af',
|
|
72
|
+
fontSize: '14px',
|
|
73
|
+
fontFamily: 'monospace',
|
|
74
|
+
padding: '4px 12px',
|
|
75
|
+
borderRadius: '4px',
|
|
76
|
+
border: 'none',
|
|
77
|
+
background: 'transparent',
|
|
78
|
+
cursor: 'pointer',
|
|
79
|
+
},
|
|
80
|
+
closeButton: {
|
|
81
|
+
color: '#9ca3af',
|
|
82
|
+
fontSize: '18px',
|
|
83
|
+
fontWeight: 'bold',
|
|
84
|
+
padding: '0 8px',
|
|
85
|
+
border: 'none',
|
|
86
|
+
background: 'transparent',
|
|
87
|
+
cursor: 'pointer',
|
|
88
|
+
borderRadius: '4px',
|
|
89
|
+
},
|
|
90
|
+
terminalsContainer: {
|
|
91
|
+
display: 'flex',
|
|
92
|
+
gap: '8px',
|
|
93
|
+
padding: '8px',
|
|
94
|
+
height: '450px',
|
|
95
|
+
},
|
|
96
|
+
terminalWrapper: {
|
|
97
|
+
flex: 1,
|
|
98
|
+
display: 'flex',
|
|
99
|
+
flexDirection: 'column' as const,
|
|
100
|
+
},
|
|
101
|
+
} as const;
|
|
102
|
+
|
|
103
|
+
export const NetworkTerminal: React.FC<NetworkTerminalProps> = ({
|
|
104
|
+
maxLogs = 100,
|
|
105
|
+
defaultVisible = false,
|
|
106
|
+
position = 'bottom',
|
|
107
|
+
height = '450px',
|
|
108
|
+
zIndex = 9999,
|
|
109
|
+
}) => {
|
|
110
|
+
const [logs, setLogs] = useState<NetworkLog[]>([]);
|
|
111
|
+
const [isVisible, setIsVisible] = useState(defaultVisible);
|
|
112
|
+
const [requestExpanded, setRequestExpanded] = useState(true);
|
|
113
|
+
const [responseExpanded, setResponseExpanded] = useState(true);
|
|
114
|
+
|
|
115
|
+
const addLog = useCallback(
|
|
116
|
+
(log: NetworkLog) => {
|
|
117
|
+
setLogs((prev) => [...prev.slice(-(maxLogs - 1)), log]);
|
|
118
|
+
},
|
|
119
|
+
[maxLogs]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const updateLog = useCallback((id: string, updates: Partial<NetworkLog>) => {
|
|
123
|
+
setLogs((prev) =>
|
|
124
|
+
prev.map((log) => (log.id === id ? { ...log, ...updates } : log))
|
|
125
|
+
);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
useNetworkInterceptor({
|
|
129
|
+
enabled: isVisible,
|
|
130
|
+
onLogAdd: addLog,
|
|
131
|
+
onLogUpdate: updateLog,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
136
|
+
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
setIsVisible((prev) => !prev);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
143
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const clearLogs = () => setLogs([]);
|
|
147
|
+
|
|
148
|
+
if (!isVisible) {
|
|
149
|
+
return (
|
|
150
|
+
<button
|
|
151
|
+
onClick={() => setIsVisible(true)}
|
|
152
|
+
style={{
|
|
153
|
+
...styles.floatingButton,
|
|
154
|
+
zIndex,
|
|
155
|
+
}}
|
|
156
|
+
title="Open Network Terminal (Ctrl+Shift+N)"
|
|
157
|
+
>
|
|
158
|
+
{'>'}_ Network
|
|
159
|
+
</button>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const containerStyle =
|
|
164
|
+
position === 'top'
|
|
165
|
+
? { ...styles.containerTop, zIndex }
|
|
166
|
+
: { ...styles.container, zIndex };
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div style={containerStyle}>
|
|
170
|
+
<div style={styles.mainHeader}>
|
|
171
|
+
<div style={styles.headerLeft}>
|
|
172
|
+
<span style={styles.title}>{'>'}_ Network Terminal</span>
|
|
173
|
+
<span style={styles.hint}>Press Ctrl+Shift+N to toggle</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div style={styles.headerRight}>
|
|
176
|
+
<button
|
|
177
|
+
onClick={clearLogs}
|
|
178
|
+
style={styles.clearButton}
|
|
179
|
+
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
|
|
180
|
+
onMouseLeave={(e) => (e.currentTarget.style.color = '#9ca3af')}
|
|
181
|
+
>
|
|
182
|
+
Clear All
|
|
183
|
+
</button>
|
|
184
|
+
<button
|
|
185
|
+
onClick={() => setIsVisible(false)}
|
|
186
|
+
style={styles.closeButton}
|
|
187
|
+
onMouseEnter={(e) => (e.currentTarget.style.color = '#f87171')}
|
|
188
|
+
onMouseLeave={(e) => (e.currentTarget.style.color = '#9ca3af')}
|
|
189
|
+
>
|
|
190
|
+
{'\u00D7'}
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div style={{ ...styles.terminalsContainer, height }}>
|
|
196
|
+
<div style={styles.terminalWrapper}>
|
|
197
|
+
<Terminal
|
|
198
|
+
title="Requests"
|
|
199
|
+
logs={logs}
|
|
200
|
+
type="request"
|
|
201
|
+
onClear={clearLogs}
|
|
202
|
+
expanded={requestExpanded}
|
|
203
|
+
onToggleExpand={() => setRequestExpanded(!requestExpanded)}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
<div style={styles.terminalWrapper}>
|
|
207
|
+
<Terminal
|
|
208
|
+
title="Responses"
|
|
209
|
+
logs={logs}
|
|
210
|
+
type="response"
|
|
211
|
+
onClear={clearLogs}
|
|
212
|
+
expanded={responseExpanded}
|
|
213
|
+
onToggleExpand={() => setResponseExpanded(!responseExpanded)}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect } from 'react';
|
|
2
|
+
import { TerminalProps } from '../types';
|
|
3
|
+
import { TerminalHeader } from './TerminalHeader';
|
|
4
|
+
import { LogEntry } from './LogEntry';
|
|
5
|
+
|
|
6
|
+
const styles = {
|
|
7
|
+
container: {
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: 'column' as const,
|
|
10
|
+
backgroundColor: '#111827',
|
|
11
|
+
borderRadius: '8px',
|
|
12
|
+
border: '1px solid #374151',
|
|
13
|
+
overflow: 'hidden',
|
|
14
|
+
transition: 'all 0.2s',
|
|
15
|
+
flex: 1,
|
|
16
|
+
},
|
|
17
|
+
collapsed: {
|
|
18
|
+
height: '48px',
|
|
19
|
+
flex: 'none' as const,
|
|
20
|
+
},
|
|
21
|
+
body: {
|
|
22
|
+
flex: 1,
|
|
23
|
+
overflow: 'auto',
|
|
24
|
+
padding: '16px',
|
|
25
|
+
fontFamily: 'monospace',
|
|
26
|
+
fontSize: '14px',
|
|
27
|
+
minHeight: '200px',
|
|
28
|
+
maxHeight: '400px',
|
|
29
|
+
},
|
|
30
|
+
empty: {
|
|
31
|
+
color: '#6b7280',
|
|
32
|
+
fontStyle: 'italic',
|
|
33
|
+
},
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export const Terminal: React.FC<TerminalProps> = ({
|
|
37
|
+
title,
|
|
38
|
+
logs,
|
|
39
|
+
type,
|
|
40
|
+
onClear,
|
|
41
|
+
expanded,
|
|
42
|
+
onToggleExpand,
|
|
43
|
+
}) => {
|
|
44
|
+
const terminalRef = useRef<HTMLDivElement>(null);
|
|
45
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (autoScroll && terminalRef.current) {
|
|
49
|
+
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
|
|
50
|
+
}
|
|
51
|
+
}, [logs, autoScroll]);
|
|
52
|
+
|
|
53
|
+
const handleScroll = () => {
|
|
54
|
+
if (terminalRef.current) {
|
|
55
|
+
const { scrollTop, scrollHeight, clientHeight } = terminalRef.current;
|
|
56
|
+
setAutoScroll(scrollHeight - scrollTop - clientHeight < 50);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
...styles.container,
|
|
64
|
+
...(expanded ? {} : styles.collapsed),
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<TerminalHeader
|
|
68
|
+
title={title}
|
|
69
|
+
count={logs.length}
|
|
70
|
+
expanded={expanded}
|
|
71
|
+
onClear={onClear}
|
|
72
|
+
onToggleExpand={onToggleExpand}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
{expanded && (
|
|
76
|
+
<div ref={terminalRef} onScroll={handleScroll} style={styles.body}>
|
|
77
|
+
{logs.length === 0 ? (
|
|
78
|
+
<div style={styles.empty}>Waiting for network requests...</div>
|
|
79
|
+
) : (
|
|
80
|
+
logs.map((log) => <LogEntry key={log.id} log={log} type={type} />)
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TerminalHeaderProps } from '../types';
|
|
3
|
+
|
|
4
|
+
const styles = {
|
|
5
|
+
header: {
|
|
6
|
+
display: 'flex',
|
|
7
|
+
alignItems: 'center',
|
|
8
|
+
justifyContent: 'space-between',
|
|
9
|
+
padding: '8px 16px',
|
|
10
|
+
backgroundColor: '#1f2937',
|
|
11
|
+
borderBottom: '1px solid #374151',
|
|
12
|
+
},
|
|
13
|
+
left: {
|
|
14
|
+
display: 'flex',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
gap: '8px',
|
|
17
|
+
},
|
|
18
|
+
trafficLights: {
|
|
19
|
+
display: 'flex',
|
|
20
|
+
gap: '6px',
|
|
21
|
+
},
|
|
22
|
+
dot: {
|
|
23
|
+
width: '12px',
|
|
24
|
+
height: '12px',
|
|
25
|
+
borderRadius: '50%',
|
|
26
|
+
},
|
|
27
|
+
title: {
|
|
28
|
+
color: '#d1d5db',
|
|
29
|
+
fontFamily: 'monospace',
|
|
30
|
+
fontSize: '14px',
|
|
31
|
+
marginLeft: '8px',
|
|
32
|
+
},
|
|
33
|
+
count: {
|
|
34
|
+
color: '#6b7280',
|
|
35
|
+
fontFamily: 'monospace',
|
|
36
|
+
fontSize: '12px',
|
|
37
|
+
},
|
|
38
|
+
right: {
|
|
39
|
+
display: 'flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
gap: '8px',
|
|
42
|
+
},
|
|
43
|
+
button: {
|
|
44
|
+
color: '#9ca3af',
|
|
45
|
+
fontSize: '12px',
|
|
46
|
+
fontFamily: 'monospace',
|
|
47
|
+
padding: '4px 8px',
|
|
48
|
+
borderRadius: '4px',
|
|
49
|
+
border: 'none',
|
|
50
|
+
background: 'transparent',
|
|
51
|
+
cursor: 'pointer',
|
|
52
|
+
},
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export const TerminalHeader: React.FC<TerminalHeaderProps> = ({
|
|
56
|
+
title,
|
|
57
|
+
count,
|
|
58
|
+
expanded,
|
|
59
|
+
onClear,
|
|
60
|
+
onToggleExpand,
|
|
61
|
+
}) => {
|
|
62
|
+
return (
|
|
63
|
+
<div style={styles.header}>
|
|
64
|
+
<div style={styles.left}>
|
|
65
|
+
<div style={styles.trafficLights}>
|
|
66
|
+
<div style={{ ...styles.dot, backgroundColor: '#ef4444' }} />
|
|
67
|
+
<div style={{ ...styles.dot, backgroundColor: '#eab308' }} />
|
|
68
|
+
<div style={{ ...styles.dot, backgroundColor: '#22c55e' }} />
|
|
69
|
+
</div>
|
|
70
|
+
<span style={styles.title}>{title}</span>
|
|
71
|
+
<span style={styles.count}>({count} entries)</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div style={styles.right}>
|
|
74
|
+
<button
|
|
75
|
+
onClick={onClear}
|
|
76
|
+
style={styles.button}
|
|
77
|
+
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
|
|
78
|
+
onMouseLeave={(e) => (e.currentTarget.style.color = '#9ca3af')}
|
|
79
|
+
>
|
|
80
|
+
Clear
|
|
81
|
+
</button>
|
|
82
|
+
<button
|
|
83
|
+
onClick={onToggleExpand}
|
|
84
|
+
style={styles.button}
|
|
85
|
+
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
|
|
86
|
+
onMouseLeave={(e) => (e.currentTarget.style.color = '#9ca3af')}
|
|
87
|
+
>
|
|
88
|
+
{expanded ? '\u25BC' : '\u25B2'}
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { NetworkLog, UseNetworkInterceptorProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export const useNetworkInterceptor = ({
|
|
5
|
+
enabled,
|
|
6
|
+
onLogAdd,
|
|
7
|
+
onLogUpdate,
|
|
8
|
+
}: UseNetworkInterceptorProps) => {
|
|
9
|
+
const originalFetch = useRef<typeof fetch | null>(null);
|
|
10
|
+
const originalXHROpen = useRef<typeof XMLHttpRequest.prototype.open | null>(null);
|
|
11
|
+
const originalXHRSend = useRef<typeof XMLHttpRequest.prototype.send | null>(null);
|
|
12
|
+
|
|
13
|
+
const generateId = useCallback((prefix: string) => {
|
|
14
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
// Intercept Fetch
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!enabled) return;
|
|
20
|
+
|
|
21
|
+
originalFetch.current = window.fetch;
|
|
22
|
+
|
|
23
|
+
window.fetch = async function (input, init) {
|
|
24
|
+
const id = generateId('fetch');
|
|
25
|
+
const startTime = performance.now();
|
|
26
|
+
|
|
27
|
+
const url =
|
|
28
|
+
typeof input === 'string'
|
|
29
|
+
? input
|
|
30
|
+
: input instanceof URL
|
|
31
|
+
? input.href
|
|
32
|
+
: input.url;
|
|
33
|
+
|
|
34
|
+
const method = init?.method || 'GET';
|
|
35
|
+
|
|
36
|
+
let requestBody: unknown = null;
|
|
37
|
+
if (init?.body) {
|
|
38
|
+
try {
|
|
39
|
+
requestBody =
|
|
40
|
+
typeof init.body === 'string' ? JSON.parse(init.body) : init.body;
|
|
41
|
+
} catch {
|
|
42
|
+
requestBody = init.body;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onLogAdd({
|
|
47
|
+
id,
|
|
48
|
+
timestamp: new Date(),
|
|
49
|
+
method: method.toUpperCase(),
|
|
50
|
+
url,
|
|
51
|
+
requestBody,
|
|
52
|
+
type: 'fetch',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const response = await originalFetch.current!(input, init);
|
|
57
|
+
const duration = Math.round(performance.now() - startTime);
|
|
58
|
+
|
|
59
|
+
const clonedResponse = response.clone();
|
|
60
|
+
let responseBody: unknown = null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
responseBody = await clonedResponse.json();
|
|
64
|
+
} catch {
|
|
65
|
+
try {
|
|
66
|
+
responseBody = await clonedResponse.text();
|
|
67
|
+
} catch {
|
|
68
|
+
responseBody = '[Could not read response body]';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onLogUpdate(id, {
|
|
73
|
+
status: response.status,
|
|
74
|
+
statusText: response.statusText,
|
|
75
|
+
responseBody,
|
|
76
|
+
duration,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return response;
|
|
80
|
+
} catch (error: unknown) {
|
|
81
|
+
const duration = Math.round(performance.now() - startTime);
|
|
82
|
+
const errorMessage =
|
|
83
|
+
error instanceof Error ? error.message : 'Network Error';
|
|
84
|
+
onLogUpdate(id, {
|
|
85
|
+
error: errorMessage,
|
|
86
|
+
duration,
|
|
87
|
+
});
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
if (originalFetch.current) {
|
|
94
|
+
window.fetch = originalFetch.current;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}, [enabled, onLogAdd, onLogUpdate, generateId]);
|
|
98
|
+
|
|
99
|
+
// Intercept XHR
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!enabled) return;
|
|
102
|
+
|
|
103
|
+
originalXHROpen.current = XMLHttpRequest.prototype.open;
|
|
104
|
+
originalXHRSend.current = XMLHttpRequest.prototype.send;
|
|
105
|
+
|
|
106
|
+
XMLHttpRequest.prototype.open = function (
|
|
107
|
+
method: string,
|
|
108
|
+
url: string | URL
|
|
109
|
+
) {
|
|
110
|
+
(this as XMLHttpRequest & { _networkTerminal: NetworkLog & { startTime: number } })._networkTerminal = {
|
|
111
|
+
id: generateId('xhr'),
|
|
112
|
+
method: method.toUpperCase(),
|
|
113
|
+
url: url.toString(),
|
|
114
|
+
startTime: 0,
|
|
115
|
+
timestamp: new Date(),
|
|
116
|
+
type: 'xhr',
|
|
117
|
+
};
|
|
118
|
+
return originalXHROpen.current!.apply(
|
|
119
|
+
this,
|
|
120
|
+
arguments as unknown as Parameters<typeof XMLHttpRequest.prototype.open>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {
|
|
125
|
+
const meta = (this as XMLHttpRequest & { _networkTerminal?: NetworkLog & { startTime: number } })._networkTerminal;
|
|
126
|
+
|
|
127
|
+
if (meta) {
|
|
128
|
+
meta.startTime = performance.now();
|
|
129
|
+
|
|
130
|
+
let requestBody: unknown = null;
|
|
131
|
+
if (body) {
|
|
132
|
+
try {
|
|
133
|
+
requestBody = typeof body === 'string' ? JSON.parse(body) : body;
|
|
134
|
+
} catch {
|
|
135
|
+
requestBody = body;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onLogAdd({
|
|
140
|
+
id: meta.id,
|
|
141
|
+
timestamp: new Date(),
|
|
142
|
+
method: meta.method,
|
|
143
|
+
url: meta.url,
|
|
144
|
+
requestBody,
|
|
145
|
+
type: 'xhr',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this.addEventListener('load', function () {
|
|
149
|
+
const duration = Math.round(performance.now() - meta.startTime);
|
|
150
|
+
|
|
151
|
+
let responseBody: unknown = null;
|
|
152
|
+
try {
|
|
153
|
+
responseBody = JSON.parse(this.responseText);
|
|
154
|
+
} catch {
|
|
155
|
+
responseBody = this.responseText;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
onLogUpdate(meta.id, {
|
|
159
|
+
status: this.status,
|
|
160
|
+
statusText: this.statusText,
|
|
161
|
+
responseBody,
|
|
162
|
+
duration,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
this.addEventListener('error', function () {
|
|
167
|
+
const duration = Math.round(performance.now() - meta.startTime);
|
|
168
|
+
onLogUpdate(meta.id, {
|
|
169
|
+
error: 'Network Error',
|
|
170
|
+
duration,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return originalXHRSend.current!.apply(
|
|
176
|
+
this,
|
|
177
|
+
arguments as unknown as Parameters<typeof XMLHttpRequest.prototype.send>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
if (originalXHROpen.current) {
|
|
183
|
+
XMLHttpRequest.prototype.open = originalXHROpen.current;
|
|
184
|
+
}
|
|
185
|
+
if (originalXHRSend.current) {
|
|
186
|
+
XMLHttpRequest.prototype.send = originalXHRSend.current;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}, [enabled, onLogAdd, onLogUpdate, generateId]);
|
|
190
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { NetworkTerminal } from './components/NetworkTerminal';
|
|
2
|
+
export { Terminal } from './components/Terminal';
|
|
3
|
+
export { TerminalHeader } from './components/TerminalHeader';
|
|
4
|
+
export { LogEntry } from './components/LogEntry';
|
|
5
|
+
export { useNetworkInterceptor } from './hooks/useNetworkInterceptor';
|
|
6
|
+
export { formatJson, truncate, formatTime } from './utils/formatters';
|
|
7
|
+
export { getStatusColor, getMethodColor } from './utils/colors';
|
|
8
|
+
export { networkTerminal } from './vite-plugin';
|
|
9
|
+
export type {
|
|
10
|
+
NetworkLog,
|
|
11
|
+
NetworkTerminalProps,
|
|
12
|
+
TerminalProps,
|
|
13
|
+
TerminalHeaderProps,
|
|
14
|
+
LogEntryProps,
|
|
15
|
+
UseNetworkInterceptorProps,
|
|
16
|
+
} from './types';
|
|
17
|
+
export type { NetworkTerminalPluginOptions } from './vite-plugin';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface NetworkLog {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: Date;
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
status?: number;
|
|
7
|
+
statusText?: string;
|
|
8
|
+
requestHeaders?: Record<string, string>;
|
|
9
|
+
requestBody?: unknown;
|
|
10
|
+
responseBody?: unknown;
|
|
11
|
+
duration?: number;
|
|
12
|
+
error?: string;
|
|
13
|
+
type: 'fetch' | 'xhr';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TerminalProps {
|
|
17
|
+
title: string;
|
|
18
|
+
logs: NetworkLog[];
|
|
19
|
+
type: 'request' | 'response';
|
|
20
|
+
onClear: () => void;
|
|
21
|
+
expanded: boolean;
|
|
22
|
+
onToggleExpand: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LogEntryProps {
|
|
26
|
+
log: NetworkLog;
|
|
27
|
+
type: 'request' | 'response';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TerminalHeaderProps {
|
|
31
|
+
title: string;
|
|
32
|
+
count: number;
|
|
33
|
+
expanded: boolean;
|
|
34
|
+
onClear: () => void;
|
|
35
|
+
onToggleExpand: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface UseNetworkInterceptorProps {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
onLogAdd: (log: NetworkLog) => void;
|
|
41
|
+
onLogUpdate: (id: string, updates: Partial<NetworkLog>) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NetworkTerminalProps {
|
|
45
|
+
maxLogs?: number;
|
|
46
|
+
defaultVisible?: boolean;
|
|
47
|
+
position?: 'bottom' | 'top';
|
|
48
|
+
height?: string;
|
|
49
|
+
zIndex?: number;
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const getStatusColor = (status?: number): string => {
|
|
2
|
+
if (!status) return '#9ca3af'; // gray-400
|
|
3
|
+
if (status >= 200 && status < 300) return '#4ade80'; // green-400
|
|
4
|
+
if (status >= 300 && status < 400) return '#facc15'; // yellow-400
|
|
5
|
+
if (status >= 400 && status < 500) return '#fb923c'; // orange-400
|
|
6
|
+
return '#f87171'; // red-400
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getMethodColor = (method: string): string => {
|
|
10
|
+
const colors: Record<string, string> = {
|
|
11
|
+
GET: '#22d3ee', // cyan-400
|
|
12
|
+
POST: '#4ade80', // green-400
|
|
13
|
+
PUT: '#facc15', // yellow-400
|
|
14
|
+
PATCH: '#fb923c', // orange-400
|
|
15
|
+
DELETE: '#f87171', // red-400
|
|
16
|
+
};
|
|
17
|
+
return colors[method.toUpperCase()] || '#9ca3af';
|
|
18
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const formatJson = (data: unknown): string => {
|
|
2
|
+
if (!data) return '';
|
|
3
|
+
try {
|
|
4
|
+
if (typeof data === 'string') {
|
|
5
|
+
const parsed = JSON.parse(data);
|
|
6
|
+
return JSON.stringify(parsed, null, 2);
|
|
7
|
+
}
|
|
8
|
+
return JSON.stringify(data, null, 2);
|
|
9
|
+
} catch {
|
|
10
|
+
return String(data);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const truncate = (str: string, maxLength: number): string => {
|
|
15
|
+
if (str.length <= maxLength) return str;
|
|
16
|
+
return str.slice(0, maxLength) + '...';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const formatTime = (date: Date): string => {
|
|
20
|
+
return date.toLocaleTimeString('en-US', {
|
|
21
|
+
hour12: false,
|
|
22
|
+
hour: '2-digit',
|
|
23
|
+
minute: '2-digit',
|
|
24
|
+
second: '2-digit',
|
|
25
|
+
});
|
|
26
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
export interface NetworkTerminalPluginOptions {
|
|
4
|
+
/** Position of the terminal: 'top' or 'bottom' (default: 'bottom') */
|
|
5
|
+
position?: 'top' | 'bottom';
|
|
6
|
+
/** Maximum number of logs to keep (default: 100) */
|
|
7
|
+
maxLogs?: number;
|
|
8
|
+
/** Terminal height (default: '450px') */
|
|
9
|
+
height?: string;
|
|
10
|
+
/** CSS z-index (default: 9999) */
|
|
11
|
+
zIndex?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const VIRTUAL_MODULE_ID = 'virtual:network-terminal';
|
|
15
|
+
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
16
|
+
|
|
17
|
+
export function networkTerminal(options: NetworkTerminalPluginOptions = {}): Plugin {
|
|
18
|
+
const {
|
|
19
|
+
position = 'bottom',
|
|
20
|
+
maxLogs = 100,
|
|
21
|
+
height = '450px',
|
|
22
|
+
zIndex = 9999,
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: 'vite-plugin-network-terminal',
|
|
27
|
+
apply: 'serve', // Only apply during development
|
|
28
|
+
|
|
29
|
+
resolveId(id) {
|
|
30
|
+
if (id === VIRTUAL_MODULE_ID) {
|
|
31
|
+
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
load(id) {
|
|
36
|
+
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
37
|
+
return `
|
|
38
|
+
import React from 'react';
|
|
39
|
+
import ReactDOM from 'react-dom/client';
|
|
40
|
+
import { NetworkTerminal } from 'network-terminal';
|
|
41
|
+
|
|
42
|
+
function initNetworkTerminal() {
|
|
43
|
+
const containerId = '__network-terminal-root__';
|
|
44
|
+
let container = document.getElementById(containerId);
|
|
45
|
+
|
|
46
|
+
if (!container) {
|
|
47
|
+
container = document.createElement('div');
|
|
48
|
+
container.id = containerId;
|
|
49
|
+
document.body.appendChild(container);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const root = ReactDOM.createRoot(container);
|
|
53
|
+
root.render(
|
|
54
|
+
React.createElement(NetworkTerminal, {
|
|
55
|
+
position: '${position}',
|
|
56
|
+
maxLogs: ${maxLogs},
|
|
57
|
+
height: '${height}',
|
|
58
|
+
zIndex: ${zIndex},
|
|
59
|
+
defaultVisible: false,
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (document.readyState === 'loading') {
|
|
65
|
+
document.addEventListener('DOMContentLoaded', initNetworkTerminal);
|
|
66
|
+
} else {
|
|
67
|
+
initNetworkTerminal();
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
transformIndexHtml(html) {
|
|
74
|
+
return {
|
|
75
|
+
html,
|
|
76
|
+
tags: [
|
|
77
|
+
{
|
|
78
|
+
tag: 'script',
|
|
79
|
+
attrs: { type: 'module', src: '/@id/__x00__virtual:network-terminal' },
|
|
80
|
+
injectTo: 'body',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default networkTerminal;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Network Terminal</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
background: #0f172a;
|
|
16
|
+
color: #e2e8f0;
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<div id="root"></div>
|
|
23
|
+
<script type="module" src="/main.tsx"></script>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { NetworkTerminal } from '../src';
|
|
4
|
+
|
|
5
|
+
const styles = {
|
|
6
|
+
container: {
|
|
7
|
+
padding: '40px',
|
|
8
|
+
maxWidth: '800px',
|
|
9
|
+
margin: '0 auto',
|
|
10
|
+
},
|
|
11
|
+
header: {
|
|
12
|
+
marginBottom: '40px',
|
|
13
|
+
},
|
|
14
|
+
title: {
|
|
15
|
+
fontSize: '32px',
|
|
16
|
+
fontWeight: 'bold',
|
|
17
|
+
color: '#4ade80',
|
|
18
|
+
fontFamily: 'monospace',
|
|
19
|
+
marginBottom: '8px',
|
|
20
|
+
},
|
|
21
|
+
subtitle: {
|
|
22
|
+
color: '#94a3b8',
|
|
23
|
+
fontSize: '16px',
|
|
24
|
+
},
|
|
25
|
+
section: {
|
|
26
|
+
background: '#1e293b',
|
|
27
|
+
borderRadius: '12px',
|
|
28
|
+
padding: '24px',
|
|
29
|
+
marginBottom: '24px',
|
|
30
|
+
},
|
|
31
|
+
sectionTitle: {
|
|
32
|
+
fontSize: '18px',
|
|
33
|
+
fontWeight: '600',
|
|
34
|
+
marginBottom: '16px',
|
|
35
|
+
color: '#f1f5f9',
|
|
36
|
+
},
|
|
37
|
+
buttonGroup: {
|
|
38
|
+
display: 'flex',
|
|
39
|
+
gap: '12px',
|
|
40
|
+
flexWrap: 'wrap' as const,
|
|
41
|
+
},
|
|
42
|
+
button: {
|
|
43
|
+
padding: '10px 20px',
|
|
44
|
+
borderRadius: '8px',
|
|
45
|
+
border: 'none',
|
|
46
|
+
cursor: 'pointer',
|
|
47
|
+
fontWeight: '500',
|
|
48
|
+
fontSize: '14px',
|
|
49
|
+
transition: 'all 0.2s',
|
|
50
|
+
},
|
|
51
|
+
getButton: {
|
|
52
|
+
background: '#22c55e',
|
|
53
|
+
color: '#000',
|
|
54
|
+
},
|
|
55
|
+
postButton: {
|
|
56
|
+
background: '#3b82f6',
|
|
57
|
+
color: '#fff',
|
|
58
|
+
},
|
|
59
|
+
putButton: {
|
|
60
|
+
background: '#f59e0b',
|
|
61
|
+
color: '#000',
|
|
62
|
+
},
|
|
63
|
+
deleteButton: {
|
|
64
|
+
background: '#ef4444',
|
|
65
|
+
color: '#fff',
|
|
66
|
+
},
|
|
67
|
+
errorButton: {
|
|
68
|
+
background: '#6b7280',
|
|
69
|
+
color: '#fff',
|
|
70
|
+
},
|
|
71
|
+
inputGroup: {
|
|
72
|
+
display: 'flex',
|
|
73
|
+
gap: '12px',
|
|
74
|
+
marginBottom: '16px',
|
|
75
|
+
},
|
|
76
|
+
input: {
|
|
77
|
+
flex: 1,
|
|
78
|
+
padding: '10px 16px',
|
|
79
|
+
borderRadius: '8px',
|
|
80
|
+
border: '1px solid #374151',
|
|
81
|
+
background: '#0f172a',
|
|
82
|
+
color: '#e2e8f0',
|
|
83
|
+
fontSize: '14px',
|
|
84
|
+
},
|
|
85
|
+
hint: {
|
|
86
|
+
marginTop: '24px',
|
|
87
|
+
padding: '16px',
|
|
88
|
+
background: '#0f172a',
|
|
89
|
+
borderRadius: '8px',
|
|
90
|
+
borderLeft: '4px solid #4ade80',
|
|
91
|
+
},
|
|
92
|
+
hintText: {
|
|
93
|
+
color: '#94a3b8',
|
|
94
|
+
fontSize: '14px',
|
|
95
|
+
fontFamily: 'monospace',
|
|
96
|
+
},
|
|
97
|
+
kbd: {
|
|
98
|
+
background: '#374151',
|
|
99
|
+
padding: '2px 8px',
|
|
100
|
+
borderRadius: '4px',
|
|
101
|
+
fontSize: '12px',
|
|
102
|
+
color: '#4ade80',
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function App() {
|
|
107
|
+
const [customUrl, setCustomUrl] = useState('https://jsonplaceholder.typicode.com/posts/1');
|
|
108
|
+
|
|
109
|
+
const makeRequest = async (method: string, url: string, body?: object) => {
|
|
110
|
+
try {
|
|
111
|
+
const options: RequestInit = {
|
|
112
|
+
method,
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
if (body) {
|
|
118
|
+
options.body = JSON.stringify(body);
|
|
119
|
+
}
|
|
120
|
+
await fetch(url, options);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Request failed:', error);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div style={styles.container}>
|
|
128
|
+
<div style={styles.header}>
|
|
129
|
+
<h1 style={styles.title}>>_ Network Terminal</h1>
|
|
130
|
+
<p style={styles.subtitle}>Monitor your Fetch/XHR requests in real-time</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div style={styles.section}>
|
|
134
|
+
<h2 style={styles.sectionTitle}>Quick Actions</h2>
|
|
135
|
+
<div style={styles.buttonGroup}>
|
|
136
|
+
<button
|
|
137
|
+
style={{ ...styles.button, ...styles.getButton }}
|
|
138
|
+
onClick={() => makeRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1')}
|
|
139
|
+
>
|
|
140
|
+
GET Request
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
style={{ ...styles.button, ...styles.postButton }}
|
|
144
|
+
onClick={() =>
|
|
145
|
+
makeRequest('POST', 'https://jsonplaceholder.typicode.com/posts', {
|
|
146
|
+
title: 'Test Post',
|
|
147
|
+
body: 'This is a test post body',
|
|
148
|
+
userId: 1,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
POST Request
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
style={{ ...styles.button, ...styles.putButton }}
|
|
156
|
+
onClick={() =>
|
|
157
|
+
makeRequest('PUT', 'https://jsonplaceholder.typicode.com/posts/1', {
|
|
158
|
+
id: 1,
|
|
159
|
+
title: 'Updated Title',
|
|
160
|
+
body: 'Updated body content',
|
|
161
|
+
userId: 1,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
>
|
|
165
|
+
PUT Request
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
style={{ ...styles.button, ...styles.deleteButton }}
|
|
169
|
+
onClick={() => makeRequest('DELETE', 'https://jsonplaceholder.typicode.com/posts/1')}
|
|
170
|
+
>
|
|
171
|
+
DELETE Request
|
|
172
|
+
</button>
|
|
173
|
+
<button
|
|
174
|
+
style={{ ...styles.button, ...styles.errorButton }}
|
|
175
|
+
onClick={() => makeRequest('GET', 'https://jsonplaceholder.typicode.com/invalid-endpoint')}
|
|
176
|
+
>
|
|
177
|
+
404 Error
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div style={styles.section}>
|
|
183
|
+
<h2 style={styles.sectionTitle}>Custom Request</h2>
|
|
184
|
+
<div style={styles.inputGroup}>
|
|
185
|
+
<input
|
|
186
|
+
type="text"
|
|
187
|
+
value={customUrl}
|
|
188
|
+
onChange={(e) => setCustomUrl(e.target.value)}
|
|
189
|
+
placeholder="Enter URL..."
|
|
190
|
+
style={styles.input}
|
|
191
|
+
/>
|
|
192
|
+
<button
|
|
193
|
+
style={{ ...styles.button, ...styles.getButton }}
|
|
194
|
+
onClick={() => makeRequest('GET', customUrl)}
|
|
195
|
+
>
|
|
196
|
+
Send GET
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div style={styles.hint}>
|
|
202
|
+
<p style={styles.hintText}>
|
|
203
|
+
Press <span style={styles.kbd}>Ctrl+Shift+N</span> to toggle the Network Terminal
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<NetworkTerminal defaultVisible={true} position="bottom" maxLogs={50} />
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
213
|
+
<React.StrictMode>
|
|
214
|
+
<App />
|
|
215
|
+
</React.StrictMode>
|
|
216
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
'network-terminal': path.resolve(__dirname, '../src'),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|