hifun-tools 1.3.13 → 1.3.15
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.
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { isIPv4, extractRootDomain, isDomainMatch, toStandardUrl, getOptimalDecodedString, } from "../utils";
|
|
5
|
+
// --------------------- isIPv4 -------------------------
|
|
6
|
+
describe("isIPv4", () => {
|
|
7
|
+
test("纯 IPv4", () => {
|
|
8
|
+
expect(isIPv4("192.168.1.1")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
test("带协议的 IPv4 链接", () => {
|
|
11
|
+
expect(isIPv4("http://8.8.8.8")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
test("非法 IP", () => {
|
|
14
|
+
expect(isIPv4("999.999.999.999")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
test("localhost 不能当成 IP", () => {
|
|
17
|
+
expect(isIPv4("localhost")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
// --------------------- extractRootDomain -------------------------
|
|
21
|
+
describe("extractRootDomain", () => {
|
|
22
|
+
test("普通域名", () => {
|
|
23
|
+
expect(extractRootDomain("https://sub.example.com")).toBe("example.com");
|
|
24
|
+
});
|
|
25
|
+
test("多级后缀", () => {
|
|
26
|
+
expect(extractRootDomain("a.b.example.com.cn")).toBe("example.com.cn");
|
|
27
|
+
});
|
|
28
|
+
test("无协议", () => {
|
|
29
|
+
expect(extractRootDomain("www.test.co.uk")).toBe("test.co.uk");
|
|
30
|
+
});
|
|
31
|
+
test("非法 URL 返回空", () => {
|
|
32
|
+
expect(extractRootDomain("%%%--非法")).toBe("");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
// --------------------- isDomainMatch -------------------------
|
|
36
|
+
describe("isDomainMatch", () => {
|
|
37
|
+
test("匹配根域", () => {
|
|
38
|
+
const list = ["a.example.com", "b.test.co.uk"];
|
|
39
|
+
expect(isDomainMatch(list, "https://api.example.com")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
test("多级后缀匹配", () => {
|
|
42
|
+
const list = ["node.test.co.uk"];
|
|
43
|
+
expect(isDomainMatch(list, "api.test.co.uk")).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
test("不匹配", () => {
|
|
46
|
+
const list = ["a.example.com"];
|
|
47
|
+
expect(isDomainMatch(list, "another.com")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
// --------------------- toStandardUrl -------------------------
|
|
51
|
+
describe("toStandardUrl", () => {
|
|
52
|
+
test("自动补协议", () => {
|
|
53
|
+
expect(toStandardUrl("baidu.com")).toBe("http://baidu.com");
|
|
54
|
+
});
|
|
55
|
+
test("保留原协议", () => {
|
|
56
|
+
expect(toStandardUrl("http://example.com")).toBe("http://example.com");
|
|
57
|
+
});
|
|
58
|
+
test("去除路径", () => {
|
|
59
|
+
expect(toStandardUrl("https://a.com/xxx/yyy")).toBe("https://a.com");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// --------------------- getOptimalDecodedString -------------------------
|
|
63
|
+
describe("getOptimalDecodedString", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
jest.useFakeTimers();
|
|
66
|
+
global.fetch = jest.fn(() => Promise.resolve({ ok: true }));
|
|
67
|
+
});
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
jest.clearAllMocks();
|
|
70
|
+
jest.useRealTimers();
|
|
71
|
+
});
|
|
72
|
+
test("返回最快的域名", async () => {
|
|
73
|
+
const arr = ["a.com", "b.com"];
|
|
74
|
+
const promise = getOptimalDecodedString(arr);
|
|
75
|
+
jest.advanceTimersByTime(10);
|
|
76
|
+
const res = await promise;
|
|
77
|
+
expect(res).toBe("a.com");
|
|
78
|
+
});
|
|
79
|
+
test("超时 fallback 到第一个", async () => {
|
|
80
|
+
fetch.mockImplementation(() => new Promise(() => { }));
|
|
81
|
+
const arr = ["a.com", "b.com"];
|
|
82
|
+
const promise = getOptimalDecodedString(arr);
|
|
83
|
+
jest.advanceTimersByTime(4000);
|
|
84
|
+
const res = await promise;
|
|
85
|
+
expect(res).toBe("a.com");
|
|
86
|
+
});
|
|
87
|
+
});
|
package/dist/init/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDomainMatch, toStandardUrl } from "./utils";
|
|
1
2
|
import { AesDecrypt, AesEncrypt } from "./ende";
|
|
2
3
|
type TenantConfig = {
|
|
3
4
|
PUBLIC_TITLE: string;
|
|
@@ -32,4 +33,4 @@ declare class InitCls {
|
|
|
32
33
|
AesDecrypt: typeof AesDecrypt;
|
|
33
34
|
}
|
|
34
35
|
declare const HF: InitCls;
|
|
35
|
-
export { HF };
|
|
36
|
+
export { HF, isDomainMatch, toStandardUrl, AesEncrypt, AesDecrypt };
|
package/dist/init/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getResource } from "./getResource";
|
|
2
|
-
import { getOptimalDecodedString } from "./utils";
|
|
2
|
+
import { getOptimalDecodedString, isDomainMatch, toStandardUrl } from "./utils";
|
|
3
3
|
import { AesDecrypt, AesEncrypt } from "./ende";
|
|
4
4
|
class InitCls {
|
|
5
5
|
domainBaseUrl = "";
|
|
@@ -46,9 +46,7 @@ class InitCls {
|
|
|
46
46
|
const data = JSON.parse(rawData);
|
|
47
47
|
const host = location.host;
|
|
48
48
|
const matchedEntry = Object.entries(data).find(([key]) => {
|
|
49
|
-
return (key
|
|
50
|
-
(host.startsWith("www.") && key === host.substring(4)) ||
|
|
51
|
-
(!host.startsWith("www.") && key === "www." + host));
|
|
49
|
+
return isDomainMatch([key], host);
|
|
52
50
|
});
|
|
53
51
|
if (matchedEntry) {
|
|
54
52
|
const matched = matchedEntry[1];
|
|
@@ -124,16 +122,16 @@ class InitCls {
|
|
|
124
122
|
const baseUrl = JSON.parse(AesDecrypt(configText));
|
|
125
123
|
if (Array.isArray(baseUrl)) {
|
|
126
124
|
try {
|
|
127
|
-
this.domainBaseUrl = await getOptimalDecodedString(baseUrl);
|
|
125
|
+
this.domainBaseUrl = toStandardUrl(await getOptimalDecodedString(baseUrl));
|
|
128
126
|
console.log("✅ 成功加载生产环境配置(测速选择):", this.domainBaseUrl);
|
|
129
127
|
}
|
|
130
128
|
catch (error) {
|
|
131
|
-
this.domainBaseUrl = baseUrl[0];
|
|
129
|
+
this.domainBaseUrl = toStandardUrl(baseUrl[0]);
|
|
132
130
|
console.warn("⚠️ 测速失败,使用第一个配置:", this.domainBaseUrl, error);
|
|
133
131
|
}
|
|
134
132
|
}
|
|
135
133
|
else {
|
|
136
|
-
this.domainBaseUrl = baseUrl;
|
|
134
|
+
this.domainBaseUrl = toStandardUrl(baseUrl);
|
|
137
135
|
console.log("✅ 成功加载生产环境配置:", this.domainBaseUrl);
|
|
138
136
|
}
|
|
139
137
|
return this.domainBaseUrl;
|
|
@@ -144,7 +142,7 @@ class InitCls {
|
|
|
144
142
|
location.host.includes("localhost"))) ||
|
|
145
143
|
location.origin.includes(this.localPath.replace(/\/+$/, ""))) {
|
|
146
144
|
console.info(`🏠 iggame匹配到默认链接`, this.defaultBaseUrl);
|
|
147
|
-
this.domainBaseUrl = this.defaultBaseUrl;
|
|
145
|
+
this.domainBaseUrl = toStandardUrl(this.defaultBaseUrl);
|
|
148
146
|
return this.domainBaseUrl;
|
|
149
147
|
}
|
|
150
148
|
else {
|
|
@@ -157,4 +155,4 @@ class InitCls {
|
|
|
157
155
|
AesDecrypt = AesDecrypt;
|
|
158
156
|
}
|
|
159
157
|
const HF = new InitCls();
|
|
160
|
-
export { HF };
|
|
158
|
+
export { HF, isDomainMatch, toStandardUrl, AesEncrypt, AesDecrypt };
|
package/dist/init/utils.d.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
+
export declare function isIPv4(str: string): boolean;
|
|
1
2
|
export declare function getOptimalDecodedString(encodedArray: string[]): Promise<string>;
|
|
3
|
+
/**
|
|
4
|
+
* 提取主域名(支持多级 TLD)
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractRootDomain(url: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* 判断 b 是否匹配 a[] 中任意一个域名
|
|
9
|
+
*/
|
|
10
|
+
export declare function isDomainMatch(list: string[], target: string): boolean;
|
|
11
|
+
export declare function toStandardUrl(input: string): string;
|
package/dist/init/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function isIPv4(str) {
|
|
1
|
+
export function isIPv4(str) {
|
|
2
2
|
if (str.startsWith("http://") || str.startsWith("https://")) {
|
|
3
3
|
try {
|
|
4
4
|
const url = new URL(str);
|
|
@@ -34,10 +34,7 @@ export async function getOptimalDecodedString(encodedArray) {
|
|
|
34
34
|
return resolve(targetArr[0]);
|
|
35
35
|
let hasResolved = false;
|
|
36
36
|
targetArr.forEach((url) => {
|
|
37
|
-
const
|
|
38
|
-
const testUrl = `${cleanUrl.startsWith("http")
|
|
39
|
-
? cleanUrl
|
|
40
|
-
: currentProtocol + "//" + cleanUrl}/health?t=${Date.now()}`;
|
|
37
|
+
const testUrl = `${toStandardUrl(url)}/health?t=${Date.now()}`;
|
|
41
38
|
const controller = new AbortController();
|
|
42
39
|
const timeoutId = setTimeout(() => {
|
|
43
40
|
controller.abort();
|
|
@@ -61,3 +58,75 @@ export async function getOptimalDecodedString(encodedArray) {
|
|
|
61
58
|
});
|
|
62
59
|
return isIp ? await getOptimal(ipList) : await getOptimal(domainList);
|
|
63
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* 常见多级后缀(Public Suffix List 的子集,可按需扩展)
|
|
63
|
+
* 如 .co.uk、.com.cn、.gov.cn、.org.cn 等
|
|
64
|
+
*/
|
|
65
|
+
const MULTI_LEVEL_TLDS = new Set([
|
|
66
|
+
"co.uk",
|
|
67
|
+
"org.uk",
|
|
68
|
+
"gov.uk",
|
|
69
|
+
"com.cn",
|
|
70
|
+
"net.cn",
|
|
71
|
+
"gov.cn",
|
|
72
|
+
"org.cn",
|
|
73
|
+
"co.jp",
|
|
74
|
+
"ne.jp",
|
|
75
|
+
"or.jp",
|
|
76
|
+
"go.jp",
|
|
77
|
+
"com.au",
|
|
78
|
+
"net.au",
|
|
79
|
+
"org.au",
|
|
80
|
+
"co.kr",
|
|
81
|
+
"ne.kr",
|
|
82
|
+
"or.kr",
|
|
83
|
+
]);
|
|
84
|
+
/**
|
|
85
|
+
* 提取主域名(支持多级 TLD)
|
|
86
|
+
*/
|
|
87
|
+
export function extractRootDomain(url) {
|
|
88
|
+
try {
|
|
89
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
90
|
+
url = "http://" + url;
|
|
91
|
+
}
|
|
92
|
+
const { hostname } = new URL(url);
|
|
93
|
+
const parts = hostname.split(".").filter(Boolean);
|
|
94
|
+
if (parts.length < 2)
|
|
95
|
+
return hostname;
|
|
96
|
+
const last2 = parts.slice(-2).join(".");
|
|
97
|
+
// 匹配多级 TLD(如 example.com.cn → example.com.cn)
|
|
98
|
+
if (MULTI_LEVEL_TLDS.has(last2)) {
|
|
99
|
+
if (parts.length >= 3) {
|
|
100
|
+
return parts.slice(-3).join(".");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// 通用规则:主域 = 最后两段
|
|
104
|
+
return last2;
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 判断 b 是否匹配 a[] 中任意一个域名
|
|
112
|
+
*/
|
|
113
|
+
export function isDomainMatch(list, target) {
|
|
114
|
+
const normalizedList = list.map(extractRootDomain).filter(Boolean);
|
|
115
|
+
const targetDomain = extractRootDomain(target);
|
|
116
|
+
return normalizedList.includes(targetDomain);
|
|
117
|
+
}
|
|
118
|
+
export function toStandardUrl(input) {
|
|
119
|
+
try {
|
|
120
|
+
let protocol = window?.location?.protocol || "https:";
|
|
121
|
+
// 自动补协议
|
|
122
|
+
if (!/^https?:\/\//i.test(input)) {
|
|
123
|
+
input = protocol + "//" + input;
|
|
124
|
+
}
|
|
125
|
+
const url = new URL(input);
|
|
126
|
+
// 输出 protocol://hostname
|
|
127
|
+
return `${url.protocol}//${url.hostname}`;
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return input; // 当成普通字符串返回
|
|
131
|
+
}
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hifun-tools",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.15",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,12 +9,18 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
|
-
"pub": "npm run build && npm version patch && npm publish --access public"
|
|
12
|
+
"pub": "npm run build && npm version patch && npm publish --access public",
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"test:watch": "jest --watch"
|
|
13
15
|
},
|
|
14
16
|
"files": [
|
|
15
17
|
"dist"
|
|
16
18
|
],
|
|
17
19
|
"devDependencies": {
|
|
20
|
+
"@types/jest": "^30.0.0",
|
|
21
|
+
"jest": "^30.2.0",
|
|
22
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
23
|
+
"ts-jest": "^29.4.5",
|
|
18
24
|
"typescript": "^5.6.3"
|
|
19
25
|
},
|
|
20
26
|
"dependencies": {
|