test-proxy-recorder 0.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/LICENSE +21 -0
- package/README.md +363 -0
- package/dist/ProxyServer.d.ts +39 -0
- package/dist/ProxyServer.d.ts.map +1 -0
- package/dist/ProxyServer.js +464 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +32 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +7 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/playwright/index.d.ts +48 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +92 -0
- package/dist/proxy.d.ts +2 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +8 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/utils/fileUtils.d.ts +5 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +16 -0
- package/dist/utils/httpHelpers.d.ts +4 -0
- package/dist/utils/httpHelpers.d.ts.map +1 -0
- package/dist/utils/httpHelpers.js +13 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/requestKeyGenerator.d.ts +3 -0
- package/dist/utils/requestKeyGenerator.d.ts.map +1 -0
- package/dist/utils/requestKeyGenerator.js +24 -0
- package/package.json +100 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const INTERNAL_API_URL = process.env.INTERNAL_API_URL || 'http://localhost:8100';
|
|
2
|
+
/**
|
|
3
|
+
* Set the proxy mode for a given session
|
|
4
|
+
* @param mode - The proxy mode to set (recording, replay, transparent)
|
|
5
|
+
* @param sessionId - Unique identifier for the session
|
|
6
|
+
* @param timeout - Optional timeout in milliseconds
|
|
7
|
+
*/
|
|
8
|
+
export async function setProxyMode(mode, sessionId, timeout) {
|
|
9
|
+
if (!INTERNAL_API_URL) {
|
|
10
|
+
console.warn('INTERNAL_API_URL not set, proxy mode not changed');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const body = {
|
|
15
|
+
mode,
|
|
16
|
+
id: sessionId,
|
|
17
|
+
...(timeout && { timeout }),
|
|
18
|
+
};
|
|
19
|
+
const response = await fetch(`${INTERNAL_API_URL}/__control`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
console.error(`Failed to set proxy mode to ${mode}:`, text);
|
|
27
|
+
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
28
|
+
}
|
|
29
|
+
await response.json();
|
|
30
|
+
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error(`Error setting proxy mode:`, error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate a session ID from test info
|
|
39
|
+
* @param testInfo - Playwright test info object
|
|
40
|
+
*/
|
|
41
|
+
export function generateSessionId(testInfo) {
|
|
42
|
+
return testInfo.title.toLowerCase().replaceAll(/\s+/g, '-');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Start recording for a test
|
|
46
|
+
* @param testInfo - Playwright test info object
|
|
47
|
+
*/
|
|
48
|
+
export async function startRecording(testInfo) {
|
|
49
|
+
const sessionId = generateSessionId(testInfo);
|
|
50
|
+
await setProxyMode('recording', sessionId);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start replay for a test
|
|
54
|
+
* @param testInfo - Playwright test info object
|
|
55
|
+
*/
|
|
56
|
+
export async function startReplay(testInfo) {
|
|
57
|
+
const sessionId = generateSessionId(testInfo);
|
|
58
|
+
await setProxyMode('replay', sessionId);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Stop recording/replay and return to transparent mode
|
|
62
|
+
* @param testInfo - Playwright test info object
|
|
63
|
+
*/
|
|
64
|
+
export async function stopProxy(testInfo) {
|
|
65
|
+
const sessionId = generateSessionId(testInfo);
|
|
66
|
+
await setProxyMode('transparent', sessionId);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Playwright test fixture helper for managing proxy mode
|
|
70
|
+
* Use this in beforeEach/afterEach hooks
|
|
71
|
+
*/
|
|
72
|
+
export const playwrightProxy = {
|
|
73
|
+
/**
|
|
74
|
+
* Setup before test - sets the proxy mode
|
|
75
|
+
* @param testInfo - Playwright test info object
|
|
76
|
+
* @param mode - The proxy mode to use for this test
|
|
77
|
+
*/
|
|
78
|
+
async before(testInfo, mode) {
|
|
79
|
+
const sessionId = generateSessionId(testInfo);
|
|
80
|
+
console.log('Proxy setup:', { mode, sessionId });
|
|
81
|
+
await setProxyMode(mode, sessionId);
|
|
82
|
+
},
|
|
83
|
+
/**
|
|
84
|
+
* Cleanup after test - returns to transparent mode
|
|
85
|
+
* @param testInfo - Playwright test info object
|
|
86
|
+
*/
|
|
87
|
+
async after(testInfo) {
|
|
88
|
+
const sessionId = generateSessionId(testInfo);
|
|
89
|
+
await setProxyMode('transparent', sessionId);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=index.js.map
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":""}
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { parseCliArgs } from './cli.js';
|
|
2
|
+
import { ProxyServer } from './ProxyServer.js';
|
|
3
|
+
const { targets, port, recordingsDir } = parseCliArgs();
|
|
4
|
+
const proxy = new ProxyServer(targets, recordingsDir);
|
|
5
|
+
await proxy.init();
|
|
6
|
+
proxy.listen(port);
|
|
7
|
+
console.log(`Recordings will be saved to: ${recordingsDir}`);
|
|
8
|
+
//# sourceMappingURL=proxy.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
export declare const Modes: {
|
|
3
|
+
readonly transparent: "transparent";
|
|
4
|
+
readonly record: "record";
|
|
5
|
+
readonly replay: "replay";
|
|
6
|
+
};
|
|
7
|
+
export type Mode = (typeof Modes)[keyof typeof Modes];
|
|
8
|
+
export interface ControlRequest {
|
|
9
|
+
mode: Mode;
|
|
10
|
+
id?: string;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RecordedRequest {
|
|
14
|
+
method: string;
|
|
15
|
+
url: string;
|
|
16
|
+
headers: http.IncomingHttpHeaders;
|
|
17
|
+
body: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface RecordedResponse {
|
|
20
|
+
statusCode: number;
|
|
21
|
+
headers: http.IncomingHttpHeaders;
|
|
22
|
+
body: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface Recording {
|
|
25
|
+
request: RecordedRequest;
|
|
26
|
+
response?: RecordedResponse;
|
|
27
|
+
timestamp: string;
|
|
28
|
+
key: string;
|
|
29
|
+
}
|
|
30
|
+
export interface WebSocketMessage {
|
|
31
|
+
direction: 'client-to-server' | 'server-to-client';
|
|
32
|
+
data: string;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
}
|
|
35
|
+
export interface WebSocketRecording {
|
|
36
|
+
url: string;
|
|
37
|
+
messages: WebSocketMessage[];
|
|
38
|
+
timestamp: string;
|
|
39
|
+
key: string;
|
|
40
|
+
}
|
|
41
|
+
export interface RecordingSession {
|
|
42
|
+
id: string;
|
|
43
|
+
recordings: Recording[];
|
|
44
|
+
websocketRecordings: WebSocketRecording[];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,eAAO,MAAM,KAAK;;;;CAIR,CAAC;AAEX,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO,KAAK,CAAC,CAAC;AAEtD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAClC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAClC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,eAAe,CAAC;IACzB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,kBAAkB,GAAG,kBAAkB,CAAC;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;CAC3C"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RecordingSession } from '../types.js';
|
|
2
|
+
export declare function getRecordingPath(recordingsDir: string, id: string): string;
|
|
3
|
+
export declare function loadRecordingSession(filePath: string): Promise<RecordingSession>;
|
|
4
|
+
export declare function saveRecordingSession(recordingsDir: string, session: RecordingSession): Promise<void>;
|
|
5
|
+
//# sourceMappingURL=fileUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileUtils.d.ts","sourceRoot":"","sources":["../../src/utils/fileUtils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,CAAC,CAG3B;AAED,wBAAsB,oBAAoB,CACxC,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const JSON_INDENT_SPACES = 2;
|
|
4
|
+
export function getRecordingPath(recordingsDir, id) {
|
|
5
|
+
return path.join(recordingsDir, `${id}.json`);
|
|
6
|
+
}
|
|
7
|
+
export async function loadRecordingSession(filePath) {
|
|
8
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
9
|
+
return JSON.parse(fileContent);
|
|
10
|
+
}
|
|
11
|
+
export async function saveRecordingSession(recordingsDir, session) {
|
|
12
|
+
const filePath = getRecordingPath(recordingsDir, session.id);
|
|
13
|
+
await fs.writeFile(filePath, JSON.stringify(session, null, JSON_INDENT_SPACES));
|
|
14
|
+
console.log(`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=fileUtils.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
export declare function readRequestBody(req: http.IncomingMessage): Promise<string>;
|
|
3
|
+
export declare function sendJsonResponse(res: http.ServerResponse, statusCode: number, data: Record<string, unknown>): void;
|
|
4
|
+
//# sourceMappingURL=httpHelpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpHelpers.d.ts","sourceRoot":"","sources":["../../src/utils/httpHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B,wBAAsB,eAAe,CACnC,GAAG,EAAE,IAAI,CAAC,eAAe,GACxB,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,IAAI,CAGN"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const CONTENT_TYPE_JSON = 'application/json';
|
|
2
|
+
export async function readRequestBody(req) {
|
|
3
|
+
let body = '';
|
|
4
|
+
for await (const chunk of req) {
|
|
5
|
+
body += chunk.toString();
|
|
6
|
+
}
|
|
7
|
+
return body;
|
|
8
|
+
}
|
|
9
|
+
export function sendJsonResponse(res, statusCode, data) {
|
|
10
|
+
res.writeHead(statusCode, { 'Content-Type': CONTENT_TYPE_JSON });
|
|
11
|
+
res.end(JSON.stringify(data));
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=httpHelpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestKeyGenerator.d.ts","sourceRoot":"","sources":["../../src/utils/requestKeyGenerator.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,MAAM,CASpE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const QUERY_HASH_LENGTH = 8;
|
|
2
|
+
export function generateRequestKey(req) {
|
|
3
|
+
const urlParts = req.url.split('?');
|
|
4
|
+
const pathname = urlParts[0];
|
|
5
|
+
const query = urlParts[1] || '';
|
|
6
|
+
const normalizedPath = normalizePathname(pathname);
|
|
7
|
+
const queryHash = generateQueryHash(query);
|
|
8
|
+
return `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
9
|
+
}
|
|
10
|
+
function normalizePathname(pathname) {
|
|
11
|
+
const normalized = pathname.replaceAll('/', '_').replace(/^_/, '');
|
|
12
|
+
return normalized || 'root';
|
|
13
|
+
}
|
|
14
|
+
function generateQueryHash(query) {
|
|
15
|
+
if (!query) {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
const hash = Buffer.from(query)
|
|
19
|
+
.toString('base64')
|
|
20
|
+
.replaceAll(/[^a-zA-Z0-9]/g, '')
|
|
21
|
+
.slice(0, Math.max(0, QUERY_HASH_LENGTH));
|
|
22
|
+
return `_${hash}`;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=requestKeyGenerator.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "test-proxy-recorder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./playwright": {
|
|
14
|
+
"types": "./dist/playwright/index.d.ts",
|
|
15
|
+
"import": "./dist/playwright/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"test-proxy-recorder": "./dist/proxy.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/**/*.js",
|
|
23
|
+
"dist/**/*.d.ts",
|
|
24
|
+
"dist/**/*.d.ts.map",
|
|
25
|
+
"!dist/**/*.test.*",
|
|
26
|
+
"!dist/**/*.integration.test.*",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"start": "node dist/proxy.js",
|
|
33
|
+
"dev": "tsx src/proxy.ts",
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"prepublish": "pnpm run build && pnpm run test:run && pnpm run lint",
|
|
36
|
+
"lint": "eslint src --ext .ts",
|
|
37
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest",
|
|
40
|
+
"test:run": "vitest run"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"playwright",
|
|
44
|
+
"testing",
|
|
45
|
+
"proxy",
|
|
46
|
+
"mock",
|
|
47
|
+
"record",
|
|
48
|
+
"replay",
|
|
49
|
+
"har",
|
|
50
|
+
"http",
|
|
51
|
+
"websocket",
|
|
52
|
+
"intercept",
|
|
53
|
+
"fixture",
|
|
54
|
+
"vcr",
|
|
55
|
+
"stub",
|
|
56
|
+
"e2e",
|
|
57
|
+
"integration-testing",
|
|
58
|
+
"test-automation",
|
|
59
|
+
"api-mocking",
|
|
60
|
+
"network-recording"
|
|
61
|
+
],
|
|
62
|
+
"author": "asmyshlyaev177",
|
|
63
|
+
"license": "MIT",
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "https://github.com/asmyshlyaev177/test-proxy-recorder.git"
|
|
67
|
+
},
|
|
68
|
+
"bugs": {
|
|
69
|
+
"url": "https://github.com/asmyshlyaev177/test-proxy-recorder/issues"
|
|
70
|
+
},
|
|
71
|
+
"homepage": "https://github.com/asmyshlyaev177/test-proxy-recorder#readme",
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=22.0.0"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"commander": "^12.0.0",
|
|
77
|
+
"http-proxy": "^1.18.1"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"@playwright/test": ">=1.0.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@types/http-proxy": "^1.17.15",
|
|
84
|
+
"@types/node": "^22.0.0",
|
|
85
|
+
"@types/ws": "^8.18.1",
|
|
86
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
87
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
88
|
+
"eslint": "^9.0.0",
|
|
89
|
+
"eslint-config-prettier": "^10.1.8",
|
|
90
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
91
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
92
|
+
"eslint-plugin-sonarjs": "^3.0.5",
|
|
93
|
+
"eslint-plugin-unicorn": "^62.0.0",
|
|
94
|
+
"prettier": "^3.6.2",
|
|
95
|
+
"tsx": "^4.19.0",
|
|
96
|
+
"typescript": "^5.6.0",
|
|
97
|
+
"vitest": "^3.2.4",
|
|
98
|
+
"ws": "^8.18.3"
|
|
99
|
+
}
|
|
100
|
+
}
|