@theihtisham/devtools-with-cloud 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.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +23 -0
- package/jest.config.js +7 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +22 -0
- package/package.json +82 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +105 -0
- package/prisma/seed.ts +211 -0
- package/src/app/(app)/ai/page.tsx +122 -0
- package/src/app/(app)/collections/page.tsx +155 -0
- package/src/app/(app)/environments/page.tsx +96 -0
- package/src/app/(app)/history/page.tsx +107 -0
- package/src/app/(app)/import/page.tsx +102 -0
- package/src/app/(app)/layout.tsx +60 -0
- package/src/app/(app)/settings/page.tsx +79 -0
- package/src/app/(app)/workspace/page.tsx +284 -0
- package/src/app/api/ai/discover/route.ts +17 -0
- package/src/app/api/ai/explain/route.ts +29 -0
- package/src/app/api/ai/generate-tests/route.ts +37 -0
- package/src/app/api/ai/suggest/route.ts +29 -0
- package/src/app/api/collections/[id]/route.ts +66 -0
- package/src/app/api/collections/route.ts +48 -0
- package/src/app/api/environments/route.ts +40 -0
- package/src/app/api/export/openapi/route.ts +17 -0
- package/src/app/api/export/postman/route.ts +18 -0
- package/src/app/api/import/curl/route.ts +18 -0
- package/src/app/api/import/har/route.ts +20 -0
- package/src/app/api/import/openapi/route.ts +21 -0
- package/src/app/api/import/postman/route.ts +21 -0
- package/src/app/api/proxy/route.ts +35 -0
- package/src/app/api/requests/[id]/execute/route.ts +85 -0
- package/src/app/api/requests/[id]/history/route.ts +23 -0
- package/src/app/api/requests/[id]/route.ts +66 -0
- package/src/app/api/requests/route.ts +49 -0
- package/src/app/api/workspaces/route.ts +38 -0
- package/src/app/globals.css +99 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +182 -0
- package/src/components/ai/ai-panel.tsx +65 -0
- package/src/components/ai/code-explainer.tsx +51 -0
- package/src/components/ai/endpoint-discovery.tsx +62 -0
- package/src/components/ai/test-generator.tsx +49 -0
- package/src/components/collections/collection-actions.tsx +36 -0
- package/src/components/collections/collection-tree.tsx +55 -0
- package/src/components/collections/folder-creator.tsx +54 -0
- package/src/components/landing/comparison.tsx +43 -0
- package/src/components/landing/cta.tsx +16 -0
- package/src/components/landing/features.tsx +24 -0
- package/src/components/landing/hero.tsx +23 -0
- package/src/components/response/body-viewer.tsx +33 -0
- package/src/components/response/headers-viewer.tsx +23 -0
- package/src/components/response/status-badge.tsx +25 -0
- package/src/components/response/test-results.tsx +50 -0
- package/src/components/response/timing-chart.tsx +39 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +32 -0
- package/src/components/ui/code-editor.tsx +51 -0
- package/src/components/ui/dialog.tsx +56 -0
- package/src/components/ui/dropdown.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/key-value-editor.tsx +75 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/tabs.tsx +85 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +54 -0
- package/src/components/workspace/request-panel.tsx +38 -0
- package/src/components/workspace/response-panel.tsx +81 -0
- package/src/components/workspace/sidebar.tsx +52 -0
- package/src/components/workspace/split-pane.tsx +49 -0
- package/src/components/workspace/tabs/auth-tab.tsx +94 -0
- package/src/components/workspace/tabs/body-tab.tsx +41 -0
- package/src/components/workspace/tabs/headers-tab.tsx +23 -0
- package/src/components/workspace/tabs/params-tab.tsx +23 -0
- package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
- package/src/components/workspace/url-bar.tsx +53 -0
- package/src/hooks/use-ai.ts +115 -0
- package/src/hooks/use-collection.ts +71 -0
- package/src/hooks/use-environment.ts +73 -0
- package/src/hooks/use-request.ts +111 -0
- package/src/lib/ai/endpoint-discovery.ts +158 -0
- package/src/lib/ai/explainer.ts +127 -0
- package/src/lib/ai/suggester.ts +164 -0
- package/src/lib/ai/test-generator.ts +161 -0
- package/src/lib/auth/api-key.ts +28 -0
- package/src/lib/auth/aws-sig.ts +131 -0
- package/src/lib/auth/basic.ts +17 -0
- package/src/lib/auth/bearer.ts +15 -0
- package/src/lib/auth/oauth2.ts +155 -0
- package/src/lib/auth/types.ts +16 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/env/manager.ts +32 -0
- package/src/lib/env/resolver.ts +30 -0
- package/src/lib/exporters/openapi.ts +193 -0
- package/src/lib/exporters/postman.ts +140 -0
- package/src/lib/graphql/builder.ts +249 -0
- package/src/lib/graphql/formatter.ts +147 -0
- package/src/lib/graphql/index.ts +43 -0
- package/src/lib/graphql/introspection.ts +175 -0
- package/src/lib/graphql/types.ts +99 -0
- package/src/lib/graphql/validator.ts +216 -0
- package/src/lib/http/client.ts +112 -0
- package/src/lib/http/proxy.ts +83 -0
- package/src/lib/http/request-builder.ts +214 -0
- package/src/lib/http/response-parser.ts +106 -0
- package/src/lib/http/timing.ts +63 -0
- package/src/lib/importers/curl-parser.ts +346 -0
- package/src/lib/importers/har-parser.ts +128 -0
- package/src/lib/importers/openapi.ts +324 -0
- package/src/lib/importers/postman.ts +312 -0
- package/src/lib/test-runner/assertions.ts +163 -0
- package/src/lib/test-runner/reporter.ts +90 -0
- package/src/lib/test-runner/runner.ts +69 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/content-type.ts +123 -0
- package/src/lib/utils/download.ts +53 -0
- package/src/lib/utils/errors.ts +92 -0
- package/src/lib/utils/format.ts +142 -0
- package/src/lib/utils/syntax-highlight.ts +108 -0
- package/src/lib/utils/validation.ts +231 -0
- package/src/lib/websocket/client.ts +182 -0
- package/src/lib/websocket/frames.ts +96 -0
- package/src/lib/websocket/history.ts +121 -0
- package/src/lib/websocket/index.ts +25 -0
- package/src/lib/websocket/types.ts +57 -0
- package/src/types/ai.ts +28 -0
- package/src/types/collection.ts +24 -0
- package/src/types/environment.ts +16 -0
- package/src/types/request.ts +54 -0
- package/src/types/response.ts +37 -0
- package/tailwind.config.ts +82 -0
- package/tests/lib/env/resolver.test.ts +108 -0
- package/tests/lib/graphql/builder.test.ts +349 -0
- package/tests/lib/graphql/formatter.test.ts +99 -0
- package/tests/lib/http/request-builder.test.ts +160 -0
- package/tests/lib/http/response-parser.test.ts +150 -0
- package/tests/lib/http/timing.test.ts +188 -0
- package/tests/lib/importers/curl-parser.test.ts +245 -0
- package/tests/lib/test-runner/assertions.test.ts +342 -0
- package/tests/lib/utils/cn.test.ts +46 -0
- package/tests/lib/utils/content-type.test.ts +175 -0
- package/tests/lib/utils/format.test.ts +188 -0
- package/tests/lib/utils/validation.test.ts +237 -0
- package/tests/lib/websocket/history.test.ts +186 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type { HttpMethod } from '@/lib/utils/validation';
|
|
2
|
+
|
|
3
|
+
export interface ParsedCurl {
|
|
4
|
+
method: HttpMethod;
|
|
5
|
+
url: string;
|
|
6
|
+
headers: { key: string; value: string; enabled: boolean }[];
|
|
7
|
+
params: { key: string; value: string; enabled: boolean }[];
|
|
8
|
+
body?: string;
|
|
9
|
+
bodyType?: string;
|
|
10
|
+
auth?: unknown;
|
|
11
|
+
name?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a cURL command string into a structured request object.
|
|
16
|
+
* Handles common cURL flags: -X, -H, -d, --data, --data-raw, -F, -u, -b, etc.
|
|
17
|
+
*/
|
|
18
|
+
export function parseCurl(command: string): ParsedCurl {
|
|
19
|
+
const tokens = tokenizeCurl(command);
|
|
20
|
+
|
|
21
|
+
if (tokens.length === 0) {
|
|
22
|
+
throw new Error('Empty cURL command');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Remove 'curl' from the start if present
|
|
26
|
+
if (tokens[0]?.toLowerCase() === 'curl') {
|
|
27
|
+
tokens.shift();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let method: HttpMethod = 'GET';
|
|
31
|
+
let url = '';
|
|
32
|
+
const headers: ParsedCurl['headers'] = [];
|
|
33
|
+
const params: ParsedCurl['params'] = [];
|
|
34
|
+
let body: string | undefined;
|
|
35
|
+
let bodyType: string | undefined;
|
|
36
|
+
let auth: unknown;
|
|
37
|
+
let hasData = false;
|
|
38
|
+
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < tokens.length) {
|
|
41
|
+
const token = tokens[i];
|
|
42
|
+
|
|
43
|
+
if (token === undefined) { i++; continue; }
|
|
44
|
+
|
|
45
|
+
switch (token) {
|
|
46
|
+
// Method
|
|
47
|
+
case '-X':
|
|
48
|
+
case '--request': {
|
|
49
|
+
const val = tokens[++i];
|
|
50
|
+
if (val) method = val.toUpperCase() as HttpMethod;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Headers
|
|
55
|
+
case '-H':
|
|
56
|
+
case '--header': {
|
|
57
|
+
const headerStr = tokens[++i];
|
|
58
|
+
if (headerStr) {
|
|
59
|
+
const colonIndex = headerStr.indexOf(':');
|
|
60
|
+
if (colonIndex > 0) {
|
|
61
|
+
const key = headerStr.slice(0, colonIndex).trim();
|
|
62
|
+
const value = headerStr.slice(colonIndex + 1).trim();
|
|
63
|
+
headers.push({ key, value, enabled: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Data
|
|
70
|
+
case '-d':
|
|
71
|
+
case '--data':
|
|
72
|
+
case '--data-raw':
|
|
73
|
+
case '--data-binary': {
|
|
74
|
+
const dataVal = tokens[++i];
|
|
75
|
+
if (dataVal) {
|
|
76
|
+
body = dataVal;
|
|
77
|
+
hasData = true;
|
|
78
|
+
|
|
79
|
+
// Determine body type
|
|
80
|
+
if (dataVal.trim().startsWith('{') || dataVal.trim().startsWith('[')) {
|
|
81
|
+
bodyType = 'json';
|
|
82
|
+
} else if (dataVal.includes('=') && !dataVal.startsWith('@')) {
|
|
83
|
+
bodyType = 'urlencoded';
|
|
84
|
+
} else {
|
|
85
|
+
bodyType = 'raw';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Default to POST when data is present
|
|
89
|
+
if (method === 'GET') method = 'POST';
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Form data
|
|
95
|
+
case '-F':
|
|
96
|
+
case '--form': {
|
|
97
|
+
const formVal = tokens[++i];
|
|
98
|
+
if (formVal) {
|
|
99
|
+
bodyType = 'form';
|
|
100
|
+
if (!body) body = formVal;
|
|
101
|
+
else body += `&${formVal}`;
|
|
102
|
+
|
|
103
|
+
if (method === 'GET') method = 'POST';
|
|
104
|
+
hasData = true;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// User (Basic Auth)
|
|
110
|
+
case '-u':
|
|
111
|
+
case '--user': {
|
|
112
|
+
const userVal = tokens[++i];
|
|
113
|
+
if (userVal) {
|
|
114
|
+
const colonIndex = userVal.indexOf(':');
|
|
115
|
+
if (colonIndex > 0) {
|
|
116
|
+
auth = {
|
|
117
|
+
type: 'basic',
|
|
118
|
+
username: userVal.slice(0, colonIndex),
|
|
119
|
+
password: userVal.slice(colonIndex + 1),
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
auth = { type: 'basic', username: userVal, password: '' };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Bearer token
|
|
129
|
+
case '--oauth2-bearer': {
|
|
130
|
+
const tokenVal = tokens[++i];
|
|
131
|
+
if (tokenVal) {
|
|
132
|
+
auth = { type: 'bearer', token: tokenVal, prefix: 'Bearer' };
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Cookies
|
|
138
|
+
case '-b':
|
|
139
|
+
case '--cookie': {
|
|
140
|
+
const cookieVal = tokens[++i];
|
|
141
|
+
if (cookieVal) {
|
|
142
|
+
headers.push({ key: 'Cookie', value: cookieVal, enabled: true });
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Follow redirects
|
|
148
|
+
case '-L':
|
|
149
|
+
case '--location':
|
|
150
|
+
// Note: we'd set followRedirects = true on the request config
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
// Insecure SSL
|
|
154
|
+
case '-k':
|
|
155
|
+
case '--insecure':
|
|
156
|
+
// Note: we'd set verifySSL = false on the request config
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
// Silent/verbose (ignore)
|
|
160
|
+
case '-s':
|
|
161
|
+
case '--silent':
|
|
162
|
+
case '-v':
|
|
163
|
+
case '--verbose':
|
|
164
|
+
case '-S':
|
|
165
|
+
case '--show-error':
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
// Output file (ignore)
|
|
169
|
+
case '-o':
|
|
170
|
+
case '--output':
|
|
171
|
+
i++; // Skip the filename
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
// Max time
|
|
175
|
+
case '-m':
|
|
176
|
+
case '--max-time':
|
|
177
|
+
i++; // Skip the time value
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
// Connection timeout
|
|
181
|
+
case '--connect-timeout':
|
|
182
|
+
i++; // Skip the time value
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
// User agent
|
|
186
|
+
case '-A':
|
|
187
|
+
case '--user-agent': {
|
|
188
|
+
const uaVal = tokens[++i];
|
|
189
|
+
if (uaVal) {
|
|
190
|
+
headers.push({ key: 'User-Agent', value: uaVal, enabled: true });
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// No default method override
|
|
196
|
+
case '--compressed':
|
|
197
|
+
headers.push({ key: 'Accept-Encoding', value: 'gzip, deflate', enabled: true });
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
// URL (first non-flag argument)
|
|
202
|
+
if (!token.startsWith('-') && !url) {
|
|
203
|
+
url = token;
|
|
204
|
+
|
|
205
|
+
// Remove quotes if present
|
|
206
|
+
if ((url.startsWith("'") && url.endsWith("'")) || (url.startsWith('"') && url.endsWith('"'))) {
|
|
207
|
+
url = url.slice(1, -1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
i++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!url) {
|
|
217
|
+
throw new Error('No URL found in cURL command');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Parse query params from URL
|
|
221
|
+
try {
|
|
222
|
+
const urlObj = new URL(url);
|
|
223
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
224
|
+
params.push({ key, value, enabled: true });
|
|
225
|
+
});
|
|
226
|
+
// Use URL without query string for cleaner display
|
|
227
|
+
url = urlObj.origin + urlObj.pathname;
|
|
228
|
+
} catch {
|
|
229
|
+
// URL might have variables, keep as-is
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
method,
|
|
234
|
+
url,
|
|
235
|
+
headers,
|
|
236
|
+
params,
|
|
237
|
+
body,
|
|
238
|
+
bodyType,
|
|
239
|
+
auth,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Tokenize a cURL command string respecting quotes and escaped characters.
|
|
245
|
+
*/
|
|
246
|
+
function tokenizeCurl(command: string): string[] {
|
|
247
|
+
const tokens: string[] = [];
|
|
248
|
+
let current = '';
|
|
249
|
+
let inSingleQuote = false;
|
|
250
|
+
let inDoubleQuote = false;
|
|
251
|
+
let escaped = false;
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < command.length; i++) {
|
|
254
|
+
const char = command[i];
|
|
255
|
+
|
|
256
|
+
if (char === undefined) continue;
|
|
257
|
+
|
|
258
|
+
if (escaped) {
|
|
259
|
+
current += char;
|
|
260
|
+
escaped = false;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (char === '\\') {
|
|
265
|
+
if (inSingleQuote) {
|
|
266
|
+
current += char;
|
|
267
|
+
} else {
|
|
268
|
+
escaped = true;
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (char === "'" && !inDoubleQuote) {
|
|
274
|
+
inSingleQuote = !inSingleQuote;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (char === '"' && !inSingleQuote) {
|
|
279
|
+
inDoubleQuote = !inDoubleQuote;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (char === ' ' && !inSingleQuote && !inDoubleQuote) {
|
|
284
|
+
if (current.length > 0) {
|
|
285
|
+
tokens.push(current);
|
|
286
|
+
current = '';
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
current += char;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (current.length > 0) {
|
|
295
|
+
tokens.push(current);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return tokens;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Generate a cURL command from request data.
|
|
303
|
+
*/
|
|
304
|
+
export function generateCurl(request: {
|
|
305
|
+
method: string;
|
|
306
|
+
url: string;
|
|
307
|
+
headers?: { key: string; value: string; enabled: boolean }[];
|
|
308
|
+
body?: string;
|
|
309
|
+
bodyType?: string;
|
|
310
|
+
auth?: unknown;
|
|
311
|
+
}): string {
|
|
312
|
+
const parts: string[] = [`curl -X ${request.method}`];
|
|
313
|
+
|
|
314
|
+
// URL
|
|
315
|
+
const quotedUrl = request.url.includes(' ')
|
|
316
|
+
? `'${request.url}'`
|
|
317
|
+
: request.url;
|
|
318
|
+
parts.push(quotedUrl);
|
|
319
|
+
|
|
320
|
+
// Headers
|
|
321
|
+
if (request.headers) {
|
|
322
|
+
for (const h of request.headers) {
|
|
323
|
+
if (h.enabled && h.key) {
|
|
324
|
+
parts.push(`-H '${h.key}: ${h.value}'`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Auth
|
|
330
|
+
if (request.auth) {
|
|
331
|
+
const auth = request.auth as Record<string, string>;
|
|
332
|
+
if (auth['type'] === 'basic') {
|
|
333
|
+
parts.push(`-u '${auth['username']}:${auth['password']}'`);
|
|
334
|
+
} else if (auth['type'] === 'bearer') {
|
|
335
|
+
parts.push(`-H 'Authorization: Bearer ${auth['token']}'`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Body
|
|
340
|
+
if (request.body) {
|
|
341
|
+
const bodyStr = request.body.replace(/'/g, "'\\''");
|
|
342
|
+
parts.push(`--data-raw '${bodyStr}'`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return parts.join(' \\\n ');
|
|
346
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { HttpMethod } from '@/lib/utils/validation';
|
|
2
|
+
|
|
3
|
+
export interface ImportedHarEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
method: HttpMethod;
|
|
6
|
+
url: string;
|
|
7
|
+
status: number;
|
|
8
|
+
statusText: string;
|
|
9
|
+
headers: { key: string; value: string; enabled: boolean }[];
|
|
10
|
+
params: { key: string; value: string; enabled: boolean }[];
|
|
11
|
+
body?: string;
|
|
12
|
+
bodyType?: string;
|
|
13
|
+
responseHeaders: { key: string; value: string }[];
|
|
14
|
+
responseBody?: string;
|
|
15
|
+
responseStatus: number;
|
|
16
|
+
responseStatusText: string;
|
|
17
|
+
timing: number;
|
|
18
|
+
startedDateTime: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ImportedHar {
|
|
22
|
+
entries: ImportedHarEntry[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Import an HAR (HTTP Archive) file.
|
|
27
|
+
*/
|
|
28
|
+
export function importHar(harJson: string): ImportedHar {
|
|
29
|
+
let parsed: unknown;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(harJson);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error('Invalid HAR file: must be valid JSON');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const har = parsed as Record<string, unknown>;
|
|
37
|
+
const log = har['log'] as Record<string, unknown> | undefined;
|
|
38
|
+
|
|
39
|
+
if (!log) {
|
|
40
|
+
throw new Error('Invalid HAR file: missing "log" object');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const entries = log['entries'] as Array<Record<string, unknown>> | undefined;
|
|
44
|
+
|
|
45
|
+
if (!entries || !Array.isArray(entries)) {
|
|
46
|
+
throw new Error('Invalid HAR file: missing "entries" array');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result: ImportedHar = { entries: [] };
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const request = entry['request'] as Record<string, unknown> | undefined;
|
|
53
|
+
const response = entry['response'] as Record<string, unknown> | undefined;
|
|
54
|
+
const time = entry['time'] as number ?? 0;
|
|
55
|
+
const startedDateTime = entry['startedDateTime'] as string ?? '';
|
|
56
|
+
|
|
57
|
+
if (!request) continue;
|
|
58
|
+
|
|
59
|
+
const method = (request['method'] as string ?? 'GET').toUpperCase() as HttpMethod;
|
|
60
|
+
const url = request['url'] as string ?? '';
|
|
61
|
+
const headers = parseHarHeaders(request['headers'] as Array<Record<string, string>>);
|
|
62
|
+
const params = parseHarParams(request['queryString'] as Array<Record<string, string>>);
|
|
63
|
+
|
|
64
|
+
let body: string | undefined;
|
|
65
|
+
let bodyType: string | undefined;
|
|
66
|
+
|
|
67
|
+
const postData = request['postData'] as Record<string, unknown> | undefined;
|
|
68
|
+
if (postData) {
|
|
69
|
+
body = postData['text'] as string ?? '';
|
|
70
|
+
const mimeType = postData['mimeType'] as string ?? '';
|
|
71
|
+
if (mimeType.includes('json')) bodyType = 'json';
|
|
72
|
+
else if (mimeType.includes('urlencoded')) bodyType = 'urlencoded';
|
|
73
|
+
else if (mimeType.includes('form-data')) bodyType = 'form';
|
|
74
|
+
else bodyType = 'raw';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const responseHeaders = parseHarResponseHeaders(response?.['headers'] as Array<Record<string, string>>);
|
|
78
|
+
const responseBody = response?.['content'] as Record<string, unknown>;
|
|
79
|
+
const responseStatus = response?.['status'] as number ?? 0;
|
|
80
|
+
const responseStatusText = response?.['statusText'] as string ?? '';
|
|
81
|
+
|
|
82
|
+
result.entries.push({
|
|
83
|
+
name: `${method} ${new URL(url).pathname}`,
|
|
84
|
+
method,
|
|
85
|
+
url,
|
|
86
|
+
status: responseStatus,
|
|
87
|
+
statusText: responseStatusText,
|
|
88
|
+
headers,
|
|
89
|
+
params,
|
|
90
|
+
body,
|
|
91
|
+
bodyType,
|
|
92
|
+
responseHeaders,
|
|
93
|
+
responseBody: responseBody?.['text'] as string,
|
|
94
|
+
responseStatus,
|
|
95
|
+
responseStatusText,
|
|
96
|
+
timing: time,
|
|
97
|
+
startedDateTime,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseHarHeaders(headers: Array<Record<string, string>> | undefined): { key: string; value: string; enabled: boolean }[] {
|
|
105
|
+
if (!headers || !Array.isArray(headers)) return [];
|
|
106
|
+
return headers.map((h) => ({
|
|
107
|
+
key: h['name'] ?? '',
|
|
108
|
+
value: h['value'] ?? '',
|
|
109
|
+
enabled: true,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseHarParams(params: Array<Record<string, string>> | undefined): { key: string; value: string; enabled: boolean }[] {
|
|
114
|
+
if (!params || !Array.isArray(params)) return [];
|
|
115
|
+
return params.map((p) => ({
|
|
116
|
+
key: p['name'] ?? '',
|
|
117
|
+
value: p['value'] ?? '',
|
|
118
|
+
enabled: true,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseHarResponseHeaders(headers: Array<Record<string, string>> | undefined): { key: string; value: string }[] {
|
|
123
|
+
if (!headers || !Array.isArray(headers)) return [];
|
|
124
|
+
return headers.map((h) => ({
|
|
125
|
+
key: h['name'] ?? '',
|
|
126
|
+
value: h['value'] ?? '',
|
|
127
|
+
}));
|
|
128
|
+
}
|