feat: finalize first modular pack

This commit is contained in:
divocat
2025-10-04 01:15:12 +03:00
parent eb52d52eb4
commit d9a4f50f62
15 changed files with 241 additions and 14 deletions

View File

@@ -0,0 +1,29 @@
interface CopyToClipboardResponse {
success: boolean;
message: string;
}
export function copyToClipboard(text: string): CopyToClipboardResponse {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
return {
success: true,
message: 'Copied!',
};
} catch (err) {
const error = err as Error;
return {
success: false,
message: `Failed to copy: ${error.message}`,
};
} finally {
document.body.removeChild(textarea);
}
}

View File

@@ -0,0 +1,32 @@
import { COMMAND_TIMEOUT } from '../constants';
import { withTimeout } from './withTimeout';
interface ExecuteShellCommandParams {
command: string;
args: string[];
timeout?: number;
}
interface ExecuteShellCommandResponse {
stdout: string;
stderr: string;
code?: number;
}
export async function executeShellCommand({
command,
args,
timeout = COMMAND_TIMEOUT,
}: ExecuteShellCommandParams): Promise<ExecuteShellCommandResponse> {
try {
return withTimeout(
fs.exec(command, args),
timeout,
[command, ...args].join(' '),
);
} catch (err) {
const error = err as Error;
return { stdout: '', stderr: error?.message, code: 0 };
}
}

View File

@@ -1,3 +1,7 @@
export * from './getBaseUrl';
export * from './parseValueList';
export * from './injectGlobalStyles';
export * from './withTimeout';
export * from './executeShellCommand';
export * from './copyToClipboard';
export * from './maskIP';

View File

@@ -0,0 +1,5 @@
export function maskIP(ip: string = ''): string {
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`);
}

View File

@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import { maskIP } from '../maskIP';
export const validIPs = [
['Standard private IP', '192.168.0.1', 'XX.XX.XX.1'],
['Public IP', '8.8.8.8', 'XX.XX.XX.8'],
['Mixed digits', '10.0.255.99', 'XX.XX.XX.99'],
['Edge values', '255.255.255.255', 'XX.XX.XX.255'],
['Zeros', '0.0.0.0', 'XX.XX.XX.0'],
];
export const invalidIPs = [
['Empty string', '', ''],
['Missing octets', '192.168.1', '192.168.1'],
['Extra octets', '1.2.3.4.5', '1.2.3.4.5'],
['Letters inside', 'abc.def.ghi.jkl', 'abc.def.ghi.jkl'],
['Spaces inside', '1. 2.3.4', '1. 2.3.4'],
['Just dots', '...', '...'],
['IP with port', '127.0.0.1:8080', '127.0.0.1:8080'],
['IP with text', 'ip=192.168.0.1', 'ip=192.168.0.1'],
];
describe('maskIP', () => {
describe.each(validIPs)('Valid IPv4: %s', (_desc, ip, expected) => {
it(`masks "${ip}" → "${expected}"`, () => {
expect(maskIP(ip)).toBe(expected);
});
});
describe.each(invalidIPs)(
'Invalid or malformed IP: %s',
(_desc, ip, expected) => {
it(`returns original string for "${ip}"`, () => {
expect(maskIP(ip)).toBe(expected);
});
},
);
it('defaults to empty string if no param passed', () => {
expect(maskIP()).toBe('');
});
});

View File

@@ -0,0 +1,21 @@
export async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
operationName: string,
timeoutMessage = 'Operation timed out',
): Promise<T> {
let timeoutId;
const start = performance.now();
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutId);
const elapsed = performance.now() - start;
console.log(`[${operationName}] Execution time: ${elapsed.toFixed(2)} ms`);
}
}

15
fe-app-podkop/src/luci.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
declare global {
const fs: {
exec(
command: string,
args?: string[],
env?: Record<string, string>,
): Promise<{
stdout: string;
stderr: string;
code?: number;
}>;
};
}
export {};

View File

@@ -1,5 +1,6 @@
'use strict';
'require baseclass';
'require fs';
export * from './validators';
export * from './helpers';

View File

@@ -31,7 +31,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
'Invalid Shadowsocks URL: decoded credentials must contain method:password',
};
}
} catch (e) {
} catch (_e) {
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
return {
valid: false,
@@ -73,7 +73,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
message: 'Invalid port number. Must be between 1 and 65535',
};
}
} catch (e) {
} catch (_e) {
return { valid: false, message: 'Invalid Shadowsocks URL: parsing failed' };
}

View File

@@ -18,7 +18,7 @@ export function validateTrojanUrl(url: string): ValidationResult {
message: 'Invalid Trojan URL: must contain username, hostname and port',
};
}
} catch (e) {
} catch (_e) {
return { valid: false, message: 'Invalid Trojan URL: parsing failed' };
}

View File

@@ -14,7 +14,7 @@ export function validateUrl(
};
}
return { valid: true, message: 'Valid' };
} catch (e) {
} catch (_e) {
return { valid: false, message: 'Invalid URL format' };
}
}

View File

@@ -94,7 +94,7 @@ export function validateVlessUrl(url: string): ValidationResult {
};
}
}
} catch (e) {
} catch (_e) {
return { valid: false, message: 'Invalid VLESS URL: parsing failed' };
}