fix: update fallback (#5115)

* fix: update fallback

* test: introduce Vitest and add semver helper tests

* chore: merge vitest config into vite
This commit is contained in:
Sline
2025-10-18 15:51:34 +08:00
committed by GitHub
parent 3d09cf0666
commit c465000178
10 changed files with 661 additions and 24 deletions

View File

@@ -60,6 +60,7 @@
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
## v2.4.2

View File

@@ -30,7 +30,8 @@
"lint:fix": "eslint -c eslint.config.ts --cache --cache-location .eslintcache --fix src",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -73,9 +74,9 @@
"react-router-dom": "7.9.4",
"react-virtuoso": "^4.14.1",
"swr": "^2.3.6",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo",
"types-pac": "^1.0.3",
"zustand": "^5.0.8",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo"
"zustand": "^5.0.8"
},
"devDependencies": {
"@actions/github": "^6.0.1",
@@ -84,6 +85,7 @@
"@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.8.1",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@vitejs/plugin-legacy": "^7.2.1",
@@ -116,7 +118,8 @@
"typescript-eslint": "^8.46.1",
"vite": "^7.1.10",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.5.0"
"vite-plugin-svgr": "^4.5.0",
"vitest": "^3.2.4"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [

355
pnpm-lock.yaml generated
View File

@@ -156,6 +156,9 @@ importers:
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
'@types/node':
specifier: ^24.8.1
version: 24.8.1
'@types/react':
specifier: 19.2.2
version: 19.2.2
@@ -164,10 +167,10 @@ importers:
version: 19.2.2(@types/react@19.2.2)
'@vitejs/plugin-legacy':
specifier: ^7.2.1
version: 7.2.1(terser@5.44.0)(vite@7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))
version: 7.2.1(terser@5.44.0)(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))
'@vitejs/plugin-react':
specifier: 5.0.4
version: 5.0.4(vite@7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))
version: 5.0.4(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))
adm-zip:
specifier: ^0.5.16
version: 0.5.16
@@ -248,13 +251,16 @@ importers:
version: 8.46.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
vite:
specifier: ^7.1.10
version: 7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
version: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
vite-plugin-monaco-editor:
specifier: ^1.1.0
version: 1.1.0(monaco-editor@0.54.0)
vite-plugin-svgr:
specifier: ^4.5.0
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
packages:
@@ -1722,9 +1728,15 @@ packages:
'@types/babel__traverse@7.20.7':
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
'@types/chai@5.2.2':
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@@ -1758,6 +1770,9 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@24.8.1':
resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==}
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1953,6 +1968,35 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/mocker@3.2.4':
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/runner@3.2.4':
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
'@vitest/snapshot@3.2.4':
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -2028,6 +2072,10 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
@@ -2098,6 +2146,10 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -2124,6 +2176,10 @@ packages:
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -2140,6 +2196,10 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
check-error@2.1.1:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -2285,6 +2345,10 @@ packages:
decode-named-character-reference@1.1.0:
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -2370,6 +2434,9 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
@@ -2605,6 +2672,9 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -2615,6 +2685,10 @@ packages:
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
expect-type@1.2.2:
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
engines: {node: '>=12.0.0'}
ext@1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
@@ -3032,6 +3106,9 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
@@ -3119,6 +3196,9 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
@@ -3440,6 +3520,13 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pathval@2.0.1:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -3735,6 +3822,9 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -3768,6 +3858,12 @@ packages:
resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
engines: {node: '>=12.0.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
stop-iteration-iterator@1.1.0:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
@@ -3826,6 +3922,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
style-to-js@1.1.16:
resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==}
@@ -3875,10 +3974,28 @@ packages:
resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==}
engines: {node: '>=0.12'}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tinypool@1.1.1:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
tinyspy@4.0.4:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -3955,6 +4072,9 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
undici-types@7.14.0:
resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==}
undici@5.29.0:
resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
engines: {node: '>=14.0'}
@@ -4019,6 +4139,11 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite-plugin-monaco-editor@1.1.0:
resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==}
peerDependencies:
@@ -4069,6 +4194,34 @@ packages:
yaml:
optional: true
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@vitest/browser': 3.2.4
'@vitest/ui': 3.2.4
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/debug':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
@@ -4114,6 +4267,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -5774,10 +5932,16 @@ snapshots:
dependencies:
'@babel/types': 7.28.4
'@types/chai@5.2.2':
dependencies:
'@types/deep-eql': 4.0.2
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
'@types/deep-eql@4.0.2': {}
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.8
@@ -5809,6 +5973,10 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/node@24.8.1':
dependencies:
undici-types: 7.14.0
'@types/parse-json@4.0.2': {}
'@types/prop-types@15.7.15': {}
@@ -5983,7 +6151,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))':
'@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.28.4
'@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4)
@@ -5998,11 +6166,11 @@ snapshots:
regenerator-runtime: 0.14.1
systemjs: 6.15.1
terser: 5.44.0
vite: 7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-react@5.0.4(vite@7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))':
'@vitejs/plugin-react@5.0.4(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.28.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4)
@@ -6010,10 +6178,52 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.38
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.2
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.2.4':
dependencies:
'@vitest/utils': 3.2.4
pathe: 2.0.3
strip-literal: 3.1.0
'@vitest/snapshot@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
magic-string: 0.30.17
pathe: 2.0.3
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.4
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
loupe: 3.2.1
tinyrainbow: 2.0.0
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
@@ -6118,6 +6328,8 @@ snapshots:
is-array-buffer: 3.0.5
optional: true
assertion-error@2.0.1: {}
async-function@1.0.0:
optional: true
@@ -6201,6 +6413,8 @@ snapshots:
buffer-from@1.1.2: {}
cac@6.7.14: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -6228,6 +6442,14 @@ snapshots:
ccount@2.0.1: {}
chai@5.3.3:
dependencies:
assertion-error: 2.0.1
check-error: 2.1.1
deep-eql: 5.0.2
loupe: 3.2.1
pathval: 2.0.1
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -6241,6 +6463,8 @@ snapshots:
character-reference-invalid@2.0.1: {}
check-error@2.1.1: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -6377,6 +6601,8 @@ snapshots:
dependencies:
character-entities: 2.0.2
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
define-data-property@1.1.4:
@@ -6509,6 +6735,8 @@ snapshots:
es-errors@1.3.0: {}
es-module-lexer@1.7.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@@ -6884,6 +7112,10 @@ snapshots:
estree-walker@2.0.2: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
esutils@2.0.3: {}
event-emitter@0.3.5:
@@ -6893,6 +7125,8 @@ snapshots:
eventemitter3@5.0.1: {}
expect-type@1.2.2: {}
ext@1.7.0:
dependencies:
type: 2.7.3
@@ -7347,6 +7581,8 @@ snapshots:
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
@@ -7430,6 +7666,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
loupe@3.2.1: {}
lower-case@2.0.2:
dependencies:
tslib: 2.8.1
@@ -7904,6 +8142,10 @@ snapshots:
path-type@4.0.0: {}
pathe@2.0.3: {}
pathval@2.0.1: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -8251,6 +8493,8 @@ snapshots:
side-channel-weakmap: 1.0.2
optional: true
siginfo@2.0.0: {}
signal-exit@4.1.0: {}
slice-ansi@7.1.2:
@@ -8278,6 +8522,10 @@ snapshots:
stable-hash-x@0.2.0: {}
stackback@0.0.2: {}
std-env@3.10.0: {}
stop-iteration-iterator@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -8355,6 +8603,10 @@ snapshots:
strip-json-comments@3.1.1: {}
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
style-to-js@1.1.16:
dependencies:
style-to-object: 1.0.8
@@ -8409,11 +8661,21 @@ snapshots:
es5-ext: 0.10.64
next-tick: 1.1.0
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinypool@1.1.1: {}
tinyrainbow@2.0.0: {}
tinyspy@4.0.4: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -8511,6 +8773,8 @@ snapshots:
which-boxed-primitive: 1.1.1
optional: true
undici-types@7.14.0: {}
undici@5.29.0:
dependencies:
'@fastify/busboy': 2.1.1
@@ -8609,22 +8873,43 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
vite-node@3.2.4(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1):
dependencies:
cac: 6.7.14
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite-plugin-monaco-editor@1.1.0(monaco-editor@0.54.0):
dependencies:
monaco-editor: 0.54.0
vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)):
vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
'@svgr/core': 8.1.0(typescript@5.9.3)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
vite: 7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- rollup
- supports-color
- typescript
vite@7.1.10(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1):
vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.4
fdir: 6.5.0(picomatch@4.0.3)
@@ -8633,12 +8918,55 @@ snapshots:
rollup: 4.46.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.8.1
fsevents: 2.3.3
jiti: 2.6.1
sass: 1.93.2
terser: 5.44.0
yaml: 2.8.1
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
debug: 4.4.1
expect-type: 1.2.2
magic-string: 0.30.17
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 7.1.10(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
vite-node: 3.2.4(@types/node@24.8.1)(jiti@2.6.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 24.8.1
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
void-elements@3.1.0: {}
vscode-jsonrpc@8.2.0: {}
@@ -8705,6 +9033,11 @@ snapshots:
dependencies:
isexe: 2.0.0
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
word-wrap@1.2.5: {}
wrap-ansi@7.0.0:

View File

@@ -14,7 +14,6 @@ import {
IconButton,
Tooltip,
} from "@mui/material";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useLockFn } from "ahooks";
import { useCallback, useEffect, useMemo, useReducer } from "react";
import { useTranslation } from "react-i18next";
@@ -26,6 +25,7 @@ import { useVerge } from "@/hooks/use-verge";
import { useServiceInstaller } from "@/hooks/useServiceInstaller";
import { getSystemInfo } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { checkUpdateSafe as checkUpdate } from "@/services/update";
import { version as appVersion } from "@root/package.json";
import { EnhancedCard } from "./enhanced-card";

View File

@@ -1,9 +1,9 @@
import { Button } from "@mui/material";
import { check } from "@tauri-apps/plugin-updater";
import { useRef } from "react";
import useSWR from "swr";
import { useVerge } from "@/hooks/use-verge";
import { checkUpdateSafe } from "@/services/update";
import { DialogRef } from "../base";
import { UpdateViewer } from "../setting/mods/update-viewer";
@@ -21,7 +21,7 @@ export const UpdateButton = (props: Props) => {
const { data: updateInfo } = useSWR(
auto_check_update || auto_check_update === null ? "checkUpdate" : null,
check,
checkUpdateSafe,
{
errorRetryCount: 2,
revalidateIfStale: false,

View File

@@ -2,7 +2,6 @@ import { Box, Button, LinearProgress } from "@mui/material";
import { Event, UnlistenFn } from "@tauri-apps/api/event";
import { relaunch } from "@tauri-apps/plugin-process";
import { open as openUrl } from "@tauri-apps/plugin-shell";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useLockFn } from "ahooks";
import type { Ref } from "react";
import { useEffect, useImperativeHandle, useMemo, useState } from "react";
@@ -15,6 +14,7 @@ import { useListen } from "@/hooks/use-listen";
import { portableFlag } from "@/pages/_layout";
import { showNotice } from "@/services/noticeService";
import { useSetUpdateState, useUpdateState } from "@/services/states";
import { checkUpdateSafe as checkUpdate } from "@/services/update";
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation();

View File

@@ -1,6 +1,5 @@
import { ContentCopyRounded } from "@mui/icons-material";
import { Typography } from "@mui/material";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
@@ -15,6 +14,7 @@ import {
openLogsDir,
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { checkUpdateSafe as checkUpdate } from "@/services/update";
import { version } from "@root/package.json";
import { BackupViewer } from "./mods/backup-viewer";

140
src/services/update.test.ts Normal file
View File

@@ -0,0 +1,140 @@
import type { Update } from "@tauri-apps/plugin-updater";
import { describe, expect, it } from "vitest";
import {
compareVersions,
ensureSemver,
extractSemver,
normalizeVersion,
resolveRemoteVersion,
splitVersion,
} from "@/services/update";
import type { VersionParts } from "@/services/update";
const makeUpdate = (data: {
version?: string | null;
rawJson?: Record<string, unknown> | null;
}): Update =>
({
version: data.version ?? "",
rawJson: data.rawJson ?? {},
}) as unknown as Update;
describe("normalizeVersion", () => {
it("strips optional v prefix and trims whitespace", () => {
expect(normalizeVersion(" v1.2.3 ")).toBe("1.2.3");
expect(normalizeVersion("V2.0.0-beta")).toBe("2.0.0-beta");
});
it("returns null for empty or non-string input", () => {
expect(normalizeVersion(null)).toBeNull();
expect(normalizeVersion(" ")).toBeNull();
});
});
describe("ensureSemver", () => {
it("returns normalized semver when input is valid", () => {
expect(ensureSemver("1.2.3")).toBe("1.2.3");
expect(ensureSemver("v3.4.5-alpha.1+build.7")).toBe(
"3.4.5-alpha.1+build.7",
);
});
it("returns null for invalid versions", () => {
expect(ensureSemver("1")).toBeNull();
expect(ensureSemver("1.2.3.4")).toBeNull();
expect(ensureSemver("release-candidate")).toBeNull();
});
});
describe("extractSemver", () => {
it("finds the first semver-like string and normalizes it", () => {
expect(extractSemver("Release v1.2.3 (latest)")).toBe("1.2.3");
expect(extractSemver("tag:V2.0.0-beta+exp.sha")).toBe("2.0.0-beta+exp.sha");
});
it("returns null when no semver-like string is present", () => {
expect(extractSemver("no version available")).toBeNull();
});
});
describe("splitVersion", () => {
it("splits version into numeric main and typed prerelease parts", () => {
const parts = splitVersion("1.2.3-alpha.4.beta") as VersionParts;
expect(parts.main).toEqual([1, 2, 3]);
expect(parts.pre).toEqual(["alpha", 4, "beta"]);
});
it("returns null when version is missing", () => {
expect(splitVersion(null)).toBeNull();
});
});
describe("compareVersions", () => {
it("orders versions by numeric components", () => {
expect(compareVersions("1.2.3", "1.2.4")).toBe(-1);
expect(compareVersions("2.0.0", "1.9.9")).toBe(1);
});
it("treats release versions as newer than prereleases", () => {
expect(compareVersions("1.0.0", "1.0.0-beta")).toBe(1);
expect(compareVersions("1.0.0-beta", "1.0.0")).toBe(-1);
});
it("resolves prerelease precedence correctly", () => {
expect(compareVersions("1.0.0-beta", "1.0.0-alpha")).toBe(1);
expect(compareVersions("1.0.0-alpha.1", "1.0.0-alpha.beta")).toBe(-1);
});
it("returns null when comparison cannot be made", () => {
expect(compareVersions(null, "1.0.0")).toBeNull();
});
});
describe("resolveRemoteVersion", () => {
it("prefers direct semver value on the update object", () => {
const update = makeUpdate({ version: "v1.2.3" });
expect(resolveRemoteVersion(update)).toBe("1.2.3");
});
it("falls back through rawJson fields when primary version is missing", () => {
const update = makeUpdate({
version: "See release notes",
rawJson: {
version: "v2.3.4",
tag_name: "ignore-me",
name: "v0.0.1",
},
});
expect(resolveRemoteVersion(update)).toBe("2.3.4");
});
it("rescues version from tag_name or name when needed", () => {
const update = makeUpdate({
version: "no version here",
rawJson: {
tag_name: "release-v3.1.0",
name: "build-should-not-override",
},
});
expect(resolveRemoteVersion(update)).toBe("3.1.0");
const nameOnly = makeUpdate({
version: "invalid",
rawJson: {
name: "release v4.0.0-beta.1",
},
});
expect(resolveRemoteVersion(nameOnly)).toBe("4.0.0-beta.1");
});
it("returns null when no semver-like data is present", () => {
const update = makeUpdate({
version: "not-a-version",
rawJson: {
name: "nope",
},
});
expect(resolveRemoteVersion(update)).toBeNull();
});
});

155
src/services/update.ts Normal file
View File

@@ -0,0 +1,155 @@
import {
check,
type CheckOptions,
type Update,
} from "@tauri-apps/plugin-updater";
import { version as appVersion } from "@root/package.json";
export type VersionParts = {
main: number[];
pre: (number | string)[];
};
const SEMVER_FULL_REGEX =
/^\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
const SEMVER_SEARCH_REGEX =
/v?\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/i;
export const normalizeVersion = (
input: string | null | undefined,
): string | null => {
if (typeof input !== "string") return null;
const trimmed = input.trim();
if (!trimmed) return null;
return trimmed.replace(/^v/i, "");
};
export const ensureSemver = (
input: string | null | undefined,
): string | null => {
const normalized = normalizeVersion(input);
if (!normalized) return null;
return SEMVER_FULL_REGEX.test(normalized) ? normalized : null;
};
export const extractSemver = (
input: string | null | undefined,
): string | null => {
if (typeof input !== "string") return null;
const match = input.match(SEMVER_SEARCH_REGEX);
if (!match) return null;
return normalizeVersion(match[0]);
};
export const splitVersion = (version: string | null): VersionParts | null => {
if (!version) return null;
const [mainPart, preRelease] = version.split("-");
const main = mainPart
.split(".")
.map((part) => Number.parseInt(part, 10))
.map((num) => (Number.isNaN(num) ? 0 : num));
const pre =
preRelease?.split(".").map((token) => {
const numeric = Number.parseInt(token, 10);
return Number.isNaN(numeric) ? token : numeric;
}) ?? [];
return { main, pre };
};
const compareVersionParts = (a: VersionParts, b: VersionParts): number => {
const length = Math.max(a.main.length, b.main.length);
for (let i = 0; i < length; i += 1) {
const diff = (a.main[i] ?? 0) - (b.main[i] ?? 0);
if (diff !== 0) return diff > 0 ? 1 : -1;
}
if (a.pre.length === 0 && b.pre.length === 0) return 0;
if (a.pre.length === 0) return 1;
if (b.pre.length === 0) return -1;
const preLen = Math.max(a.pre.length, b.pre.length);
for (let i = 0; i < preLen; i += 1) {
const aToken = a.pre[i];
const bToken = b.pre[i];
if (aToken === undefined) return -1;
if (bToken === undefined) return 1;
if (typeof aToken === "number" && typeof bToken === "number") {
if (aToken > bToken) return 1;
if (aToken < bToken) return -1;
continue;
}
if (typeof aToken === "number") return -1;
if (typeof bToken === "number") return 1;
if (aToken > bToken) return 1;
if (aToken < bToken) return -1;
}
return 0;
};
export const compareVersions = (
a: string | null,
b: string | null,
): number | null => {
const partsA = splitVersion(a);
const partsB = splitVersion(b);
if (!partsA || !partsB) return null;
return compareVersionParts(partsA, partsB);
};
export const resolveRemoteVersion = (update: Update): string | null => {
const primary = ensureSemver(update.version);
if (primary) return primary;
const fallbackPrimary = extractSemver(update.version);
if (fallbackPrimary) return fallbackPrimary;
const raw = update.rawJson ?? {};
const rawVersion = ensureSemver(
typeof raw.version === "string" ? raw.version : null,
);
if (rawVersion) return rawVersion;
const tagVersion = extractSemver(
typeof raw.tag_name === "string" ? raw.tag_name : null,
);
if (tagVersion) return tagVersion;
const nameVersion = extractSemver(
typeof raw.name === "string" ? raw.name : null,
);
if (nameVersion) return nameVersion;
return null;
};
const localVersionNormalized = normalizeVersion(appVersion);
export const checkUpdateSafe = async (
options?: CheckOptions,
): Promise<Update | null> => {
const result = await check({ ...(options ?? {}), allowDowngrades: false });
if (!result) return null;
const remoteVersion = resolveRemoteVersion(result);
const comparison = compareVersions(remoteVersion, localVersionNormalized);
if (comparison !== null && comparison <= 0) {
try {
await result.close();
} catch (err) {
console.warn("[updater] failed to close stale update resource", err);
}
return null;
}
return result;
};
export type { CheckOptions };

View File

@@ -1,11 +1,12 @@
import { defineConfig } from "vite";
import path from "path";
import svgr from "vite-plugin-svgr";
import react from "@vitejs/plugin-react";
import path from "node:path";
import legacy from "@vitejs/plugin-legacy";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
import monacoEditorPlugin, {
type IMonacoEditorOpts,
} from "vite-plugin-monaco-editor";
import svgr from "vite-plugin-svgr";
const monacoEditorPluginDefault = (monacoEditorPlugin as any).default as (
options: IMonacoEditorOpts,
) => any;
@@ -159,6 +160,10 @@ export default defineConfig({
"@root": path.resolve("."),
},
},
test: {
environment: "node",
include: ["**/*.{test,spec}.{ts,tsx}"],
},
define: {
OS_PLATFORM: `"${process.platform}"`,