mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-06 03:26:51 +03:00
feat: Introduce fe modular build system
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.idea
|
||||
fe-app-podkop/node_modules
|
||||
|
||||
8
fe-app-podkop/.prettierrc
Normal file
8
fe-app-podkop/.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
18
fe-app-podkop/eslint.config.js
Normal file
18
fe-app-podkop/eslint.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// eslint.config.js
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
ignores: ['dist', 'node_modules'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
];
|
||||
25
fe-app-podkop/package.json
Normal file
25
fe-app-podkop/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "fe-app-podkop",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"format": "prettier --write src",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
||||
"build": "tsup src/main.ts",
|
||||
"dev": "tsup src/main.ts --watch",
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||
"@typescript-eslint/parser": "8.45.0",
|
||||
"eslint": "9.36.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"prettier": "3.6.2",
|
||||
"tsup": "8.5.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.45.0",
|
||||
"vitest": "3.2.4"
|
||||
}
|
||||
}
|
||||
4
fe-app-podkop/src/main.ts
Normal file
4
fe-app-podkop/src/main.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
export * from './validators';
|
||||
4
fe-app-podkop/src/validators/index.ts
Normal file
4
fe-app-podkop/src/validators/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './validateIp';
|
||||
export * from './validateDomain';
|
||||
export * from './validateDns';
|
||||
export * from './validateUrl';
|
||||
24
fe-app-podkop/src/validators/tests/validateDns.test.js
Normal file
24
fe-app-podkop/src/validators/tests/validateDns.test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { validateDNS } from '../validateDns.js';
|
||||
import { invalidIPs, validIPs } from './validateIp.test';
|
||||
import { invalidDomains, validDomains } from './validateDomain.test';
|
||||
|
||||
const validDns = [...validIPs, ...validDomains];
|
||||
|
||||
const invalidDns = [...invalidIPs, ...invalidDomains];
|
||||
|
||||
describe('validateDns', () => {
|
||||
describe.each(validDns)('Valid dns: %s', (_desc, domain) => {
|
||||
it(`returns valid=true for "${domain}"`, () => {
|
||||
const res = validateDNS(domain);
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidDns)('Invalid dns: %s', (_desc, domain) => {
|
||||
it(`returns valid=false for "${domain}"`, () => {
|
||||
const res = validateDNS(domain);
|
||||
expect(res.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
46
fe-app-podkop/src/validators/tests/validateDomain.test.js
Normal file
46
fe-app-podkop/src/validators/tests/validateDomain.test.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { validateDomain } from '../validateDomain';
|
||||
|
||||
export const validDomains = [
|
||||
['Simple domain', 'example.com'],
|
||||
['Subdomain', 'sub.example.com'],
|
||||
['With dash', 'my-site.org'],
|
||||
['With numbers', 'site123.net'],
|
||||
['Deep subdomain', 'a.b.c.example.co.uk'],
|
||||
['With path', 'example.com/path/to/resource'],
|
||||
['Punycode RU', 'xn--d1acufc.xn--p1ai'],
|
||||
['Adguard dns', 'dns.adguard-dns.com'],
|
||||
['Nextdns dns', 'dns.nextdns.io/xxxxxxx'],
|
||||
['Long domain (63 chars in label)', 'a'.repeat(63) + '.com'],
|
||||
];
|
||||
|
||||
export const invalidDomains = [
|
||||
['No TLD', 'localhost'],
|
||||
['Only TLD', '.com'],
|
||||
['Double dot', 'example..com'],
|
||||
['Illegal chars', 'exa!mple.com'],
|
||||
['Space inside', 'exa mple.com'],
|
||||
['Ending with dash', 'example-.com'],
|
||||
['Starting with dash', '-example.com'],
|
||||
['Trailing dot', 'example.com.'],
|
||||
['Too short TLD', 'example.c'],
|
||||
['With protocol (not allowed)', 'http://example.com'],
|
||||
['Too long label (>63 chars)', 'a'.repeat(64) + '.com'],
|
||||
['Too long domain (>253 chars)', Array(40).fill('abcdef').join('.') + '.com'],
|
||||
];
|
||||
|
||||
describe('validateDomain', () => {
|
||||
describe.each(validDomains)('Valid domain: %s', (_desc, domain) => {
|
||||
it(`returns valid=true for "${domain}"`, () => {
|
||||
const res = validateDomain(domain);
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidDomains)('Invalid domain: %s', (_desc, domain) => {
|
||||
it(`returns valid=false for "${domain}"`, () => {
|
||||
const res = validateDomain(domain);
|
||||
expect(res.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
fe-app-podkop/src/validators/tests/validateIp.test.js
Normal file
38
fe-app-podkop/src/validators/tests/validateIp.test.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateIPV4 } from '../validateIp';
|
||||
|
||||
export const validIPs = [
|
||||
['Private LAN', '192.168.1.1'],
|
||||
['All zeros', '0.0.0.0'],
|
||||
['Broadcast', '255.255.255.255'],
|
||||
['Simple', '1.2.3.4'],
|
||||
['Loopback', '127.0.0.1'],
|
||||
];
|
||||
|
||||
export const invalidIPs = [
|
||||
['Octet too large', '256.0.0.1'],
|
||||
['Too few octets', '192.168.1'],
|
||||
['Too many octets', '1.2.3.4.5'],
|
||||
['Leading zero (1st octet)', '01.2.3.4'],
|
||||
['Leading zero (2nd octet)', '1.02.3.4'],
|
||||
['Leading zero (3rd octet)', '1.2.003.4'],
|
||||
['Leading zero (4th octet)', '1.2.3.004'],
|
||||
['Four digits in octet', '1.2.3.0004'],
|
||||
['Trailing dot', '1.2.3.'],
|
||||
];
|
||||
|
||||
describe('validateIPV4', () => {
|
||||
describe.each(validIPs)('Valid IP: %s', (_desc, ip) => {
|
||||
it(`returns {valid:true for "${ip}"`, () => {
|
||||
const res = validateIPV4(ip);
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidIPs)('Invalid IP: %s', (_desc, ip) => {
|
||||
it(`returns {valid:false for "${ip}"`, () => {
|
||||
const res = validateIPV4(ip);
|
||||
expect(res.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
40
fe-app-podkop/src/validators/tests/validateUrl.test.js
Normal file
40
fe-app-podkop/src/validators/tests/validateUrl.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateUrl } from '../validateUrl';
|
||||
|
||||
const validUrls = [
|
||||
['Simple HTTP', 'http://example.com'],
|
||||
['Simple HTTPS', 'https://example.com'],
|
||||
['With path', 'https://example.com/path/to/page'],
|
||||
['With query', 'https://example.com/?q=test'],
|
||||
['With port', 'http://example.com:8080'],
|
||||
['With subdomain', 'https://sub.example.com'],
|
||||
];
|
||||
|
||||
const invalidUrls = [
|
||||
['Invalid format', 'not a url'],
|
||||
['Missing protocol', 'example.com'],
|
||||
['Unsupported protocol (ftp)', 'ftp://example.com'],
|
||||
['Unsupported protocol (ws)', 'ws://example.com'],
|
||||
['Empty string', ''],
|
||||
];
|
||||
|
||||
describe('validateUrl', () => {
|
||||
describe.each(validUrls)('Valid URL: %s', (_desc, url) => {
|
||||
it(`returns valid=true for "${url}"`, () => {
|
||||
const res = validateUrl(url);
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => {
|
||||
it(`returns valid=false for "${url}"`, () => {
|
||||
const res = validateUrl(url);
|
||||
expect(res.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows custom protocol list (ftp)', () => {
|
||||
const res = validateUrl('ftp://example.com', ['ftp:']);
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
4
fe-app-podkop/src/validators/types.ts
Normal file
4
fe-app-podkop/src/validators/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
message: string;
|
||||
}
|
||||
23
fe-app-podkop/src/validators/validateDns.ts
Normal file
23
fe-app-podkop/src/validators/validateDns.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { validateDomain } from './validateDomain';
|
||||
import { validateIPV4 } from './validateIp';
|
||||
import { ValidationResult } from './types.js';
|
||||
|
||||
export function validateDNS(value: string): ValidationResult {
|
||||
if (!value) {
|
||||
return { valid: false, message: 'DNS server address cannot be empty' };
|
||||
}
|
||||
|
||||
if (validateIPV4(value).valid) {
|
||||
return { valid: true, message: 'Valid' };
|
||||
}
|
||||
|
||||
if (validateDomain(value).valid) {
|
||||
return { valid: true, message: 'Valid' };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH',
|
||||
};
|
||||
}
|
||||
21
fe-app-podkop/src/validators/validateDomain.ts
Normal file
21
fe-app-podkop/src/validators/validateDomain.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ValidationResult } from './types.js';
|
||||
|
||||
export function validateDomain(domain: string): ValidationResult {
|
||||
const domainRegex =
|
||||
/^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/;
|
||||
|
||||
if (!domainRegex.test(domain)) {
|
||||
return { valid: false, message: 'Invalid domain address' };
|
||||
}
|
||||
|
||||
const hostname = domain.split('/')[0];
|
||||
const parts = hostname.split('.');
|
||||
|
||||
const atLeastOneInvalidPart = parts.some((part) => part.length > 63);
|
||||
|
||||
if (atLeastOneInvalidPart) {
|
||||
return { valid: false, message: 'Invalid domain address' };
|
||||
}
|
||||
|
||||
return { valid: true, message: 'Valid' };
|
||||
}
|
||||
12
fe-app-podkop/src/validators/validateIp.ts
Normal file
12
fe-app-podkop/src/validators/validateIp.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ValidationResult } from './types.js';
|
||||
|
||||
export function validateIPV4(ip: string): ValidationResult {
|
||||
const ipRegex =
|
||||
/^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
|
||||
|
||||
if (ipRegex.test(ip)) {
|
||||
return { valid: true, message: 'Valid' };
|
||||
}
|
||||
|
||||
return { valid: false, message: 'Invalid IP address' };
|
||||
}
|
||||
20
fe-app-podkop/src/validators/validateUrl.ts
Normal file
20
fe-app-podkop/src/validators/validateUrl.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ValidationResult } from './types.js';
|
||||
|
||||
export function validateUrl(
|
||||
url: string,
|
||||
protocols: string[] = ['http:', 'https:'],
|
||||
): ValidationResult {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
if (!protocols.includes(parsedUrl.protocol)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `URL must use one of the following protocols: ${protocols.join(', ')}`,
|
||||
};
|
||||
}
|
||||
return { valid: true, message: 'Valid' };
|
||||
} catch (e) {
|
||||
return { valid: false, message: 'Invalid URL format' };
|
||||
}
|
||||
}
|
||||
13
fe-app-podkop/tsconfig.json
Normal file
13
fe-app-podkop/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
35
fe-app-podkop/tsup.config.ts
Normal file
35
fe-app-podkop/tsup.config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/main.ts'],
|
||||
format: ['esm'], // пусть tsup генерит export {...}
|
||||
outDir: '../luci-app-podkop/htdocs/luci-static/resources/view/podkop',
|
||||
outExtension: () => ({ js: '.js' }),
|
||||
dts: false,
|
||||
clean: false,
|
||||
sourcemap: false,
|
||||
banner: {
|
||||
js: `// This file is autogenerated, please don't change manually \n"use strict";`,
|
||||
},
|
||||
esbuildOptions(options) {
|
||||
options.legalComments = 'none';
|
||||
},
|
||||
onSuccess: () => {
|
||||
const outDir =
|
||||
'../luci-app-podkop/htdocs/luci-static/resources/view/podkop';
|
||||
const file = path.join(outDir, 'main.js');
|
||||
let code = fs.readFileSync(file, 'utf8');
|
||||
|
||||
code = code.replace(
|
||||
/export\s*{([\s\S]*?)}/,
|
||||
(match, group) => {
|
||||
return `return baseclass.extend({${group}})`;
|
||||
}
|
||||
);
|
||||
|
||||
fs.writeFileSync(file, code, 'utf8');
|
||||
console.log(`✅ Patched LuCI build: ${file}`);
|
||||
},
|
||||
});
|
||||
8
fe-app-podkop/vitest.config.js
Normal file
8
fe-app-podkop/vitest.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
},
|
||||
});
|
||||
1857
fe-app-podkop/yarn.lock
Normal file
1857
fe-app-podkop/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,19 +4,9 @@
|
||||
'require ui';
|
||||
'require network';
|
||||
'require view.podkop.constants as constants';
|
||||
'require view.podkop.main as main';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
function validateUrl(url, protocols = ['http:', 'https:']) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (!protocols.includes(parsedUrl.protocol)) {
|
||||
return _('URL must use one of the following protocols: ') + protocols.join(', ');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return _('Invalid URL format');
|
||||
}
|
||||
}
|
||||
|
||||
function createConfigSection(section, map, network) {
|
||||
const s = section;
|
||||
@@ -438,8 +428,18 @@ function createConfigSection(section, map, network) {
|
||||
o.rmempty = false;
|
||||
o.ucisection = s.section;
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value || value.length === 0) return true;
|
||||
return validateUrl(value);
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const validation = main.validateUrl(url);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _(validation.message)
|
||||
};
|
||||
|
||||
o = s.taboption('basic', form.Flag, 'local_subnet_lists_enabled', _('Local Subnet Lists'), _('Use the list from the router filesystem'));
|
||||
@@ -562,8 +562,18 @@ function createConfigSection(section, map, network) {
|
||||
o.rmempty = false;
|
||||
o.ucisection = s.section;
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value || value.length === 0) return true;
|
||||
return validateUrl(value);
|
||||
// Optional
|
||||
if (!value || value.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const validation = main.validateUrl(url);
|
||||
|
||||
if (validation.valid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _(validation.message)
|
||||
};
|
||||
|
||||
o = s.taboption('basic', form.Flag, 'all_traffic_from_ip_enabled', _('IP for full redirection'), _('Specify local IP addresses whose traffic will always use the configured route'));
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// This file is autogenerated, please don't change manually
|
||||
"use strict";
|
||||
"require baseclass";
|
||||
|
||||
// src/validators/validateIp.ts
|
||||
function validateIPV4(ip) {
|
||||
const ipRegex = /^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
|
||||
if (ipRegex.test(ip)) {
|
||||
return { valid: true, message: "Valid" };
|
||||
}
|
||||
return { valid: false, message: "Invalid IP address" };
|
||||
}
|
||||
|
||||
// src/validators/validateDomain.ts
|
||||
function validateDomain(domain) {
|
||||
const domainRegex = /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/;
|
||||
if (!domainRegex.test(domain)) {
|
||||
return { valid: false, message: "Invalid domain address" };
|
||||
}
|
||||
const hostname = domain.split("/")[0];
|
||||
const parts = hostname.split(".");
|
||||
const atLeastOneInvalidPart = parts.some((part) => part.length > 63);
|
||||
if (atLeastOneInvalidPart) {
|
||||
return { valid: false, message: "Invalid domain address" };
|
||||
}
|
||||
return { valid: true, message: "Valid" };
|
||||
}
|
||||
|
||||
// src/validators/validateDns.ts
|
||||
function validateDNS(value) {
|
||||
if (!value) {
|
||||
return { valid: false, message: "DNS server address cannot be empty" };
|
||||
}
|
||||
if (validateIPV4(value).valid) {
|
||||
return { valid: true, message: "Valid" };
|
||||
}
|
||||
if (validateDomain(value).valid) {
|
||||
return { valid: true, message: "Valid" };
|
||||
}
|
||||
return {
|
||||
valid: false,
|
||||
message: "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH"
|
||||
};
|
||||
}
|
||||
|
||||
// src/validators/validateUrl.ts
|
||||
function validateUrl(url, protocols = ["http:", "https:"]) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (!protocols.includes(parsedUrl.protocol)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `URL must use one of the following protocols: ${protocols.join(", ")}`
|
||||
};
|
||||
}
|
||||
return { valid: true, message: "Valid" };
|
||||
} catch (e) {
|
||||
return { valid: false, message: "Invalid URL format" };
|
||||
}
|
||||
}
|
||||
return baseclass.extend({
|
||||
validateDNS,
|
||||
validateDomain,
|
||||
validateIPV4,
|
||||
validateUrl
|
||||
});
|
||||
Reference in New Issue
Block a user