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

@@ -7,11 +7,20 @@ export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ['dist', 'node_modules'],
ignores: ['node_modules'],
},
{
rules: {
'no-console': 'warn',
'no-console': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
prettier,

View File

@@ -9,7 +9,8 @@
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"build": "tsup src/main.ts",
"dev": "tsup src/main.ts --watch",
"test": "vitest"
"test": "vitest",
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "8.45.0",

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' };
}

View File

@@ -1,6 +1,7 @@
// This file is autogenerated, please don't change manually
"use strict";
"require baseclass";
"require fs";
// src/validators/validateIp.ts
function validateIPV4(ip) {
@@ -54,7 +55,7 @@ function validateUrl(url, protocols = ["http:", "https:"]) {
};
}
return { valid: true, message: "Valid" };
} catch (e) {
} catch (_e) {
return { valid: false, message: "Invalid URL format" };
}
}
@@ -143,7 +144,7 @@ function validateShadowsocksUrl(url) {
message: "Invalid Shadowsocks URL: decoded credentials must contain method:password"
};
}
} catch (e) {
} catch (_e) {
if (!encryptedPart.includes(":") && !encryptedPart.includes("-")) {
return {
valid: false,
@@ -176,7 +177,7 @@ function validateShadowsocksUrl(url) {
message: "Invalid port number. Must be between 1 and 65535"
};
}
} catch (e) {
} catch (_e) {
return { valid: false, message: "Invalid Shadowsocks URL: parsing failed" };
}
return { valid: true, message: "Valid" };
@@ -255,7 +256,7 @@ function validateVlessUrl(url) {
};
}
}
} catch (e) {
} catch (_e) {
return { valid: false, message: "Invalid VLESS URL: parsing failed" };
}
return { valid: true, message: "Valid" };
@@ -293,7 +294,7 @@ function validateTrojanUrl(url) {
message: "Invalid Trojan URL: must contain username, hostname and port"
};
}
} catch (e) {
} catch (_e) {
return { valid: false, message: "Invalid Trojan URL: parsing failed" };
}
return { valid: true, message: "Valid" };
@@ -366,6 +367,22 @@ function injectGlobalStyles() {
);
}
// src/helpers/withTimeout.ts
async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = "Operation timed out") {
let timeoutId;
const start = performance.now();
const timeoutPromise = new Promise((_, 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`);
}
}
// src/constants.ts
var STATUS_COLORS = {
SUCCESS: "#4caf50",
@@ -475,6 +492,53 @@ var COMMAND_SCHEDULING = {
P10_PRIORITY: 1900
// Lowest priority
};
// src/helpers/executeShellCommand.ts
async function executeShellCommand({
command,
args,
timeout = COMMAND_TIMEOUT
}) {
try {
return withTimeout(
fs.exec(command, args),
timeout,
[command, ...args].join(" ")
);
} catch (err) {
const error = err;
return { stdout: "", stderr: error?.message, code: 0 };
}
}
// src/helpers/copyToClipboard.ts
function copyToClipboard(text) {
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;
return {
success: false,
message: `Failed to copy: ${error.message}`
};
} finally {
document.body.removeChild(textarea);
}
}
// src/helpers/maskIP.ts
function maskIP(ip = "") {
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}`);
}
return baseclass.extend({
ALLOWED_WITH_RUSSIA_INSIDE,
BOOTSTRAP_DNS_SERVER_OPTIONS,
@@ -494,8 +558,11 @@ return baseclass.extend({
STATUS_COLORS,
UPDATE_INTERVAL_OPTIONS,
bulkValidate,
copyToClipboard,
executeShellCommand,
getBaseUrl,
injectGlobalStyles,
maskIP,
parseValueList,
validateDNS,
validateDomain,
@@ -507,5 +574,6 @@ return baseclass.extend({
validateSubnet,
validateTrojanUrl,
validateUrl,
validateVlessUrl
validateVlessUrl,
withTimeout
});