hifun-tools 1.3.12 → 1.3.14

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,4 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ export {};
@@ -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
+ });
@@ -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 === host ||
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,8 @@ 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);
146
+ return this.domainBaseUrl;
148
147
  }
149
148
  else {
150
149
  console.warn("⚠️ 加载 lineAddress.txt 失败:", error);
@@ -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;
@@ -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 cleanUrl = url.replace(/\/+$/, "");
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.12",
3
+ "version": "1.3.14",
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": {