From 37ffa262ae0ca107eb1cea1e8fca5414f698b088 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Wed, 18 Mar 2026 22:00:25 +0800 Subject: [PATCH] refactor: move to std-semver (#33682) --- .../plugins/plugin-install-flow.test.ts | 27 ------------------ .../install-plugin/__tests__/hooks.spec.ts | 28 ------------------- .../steps/install.tsx | 8 +++--- .../steps/install.tsx | 8 +++--- .../components/plugins/plugin-item/index.tsx | 18 ++++++------ .../update-plugin/__tests__/index.spec.tsx | 14 ---------- .../update-plugin/plugin-version-picker.tsx | 4 +-- web/eslint-suppressions.json | 13 --------- web/package.json | 3 +- web/pnpm-lock.yaml | 20 ++++++------- web/utils/semver.spec.ts | 22 ++++++++++++++- web/utils/semver.ts | 14 +++++++--- 12 files changed, 60 insertions(+), 119 deletions(-) diff --git a/web/__tests__/plugins/plugin-install-flow.test.ts b/web/__tests__/plugins/plugin-install-flow.test.ts index 7ceca4535b..8edb6705d4 100644 --- a/web/__tests__/plugins/plugin-install-flow.test.ts +++ b/web/__tests__/plugins/plugin-install-flow.test.ts @@ -22,33 +22,6 @@ vi.mock('@/service/plugins', () => ({ checkTaskStatus: vi.fn(), })) -vi.mock('@/utils/semver', () => ({ - compareVersion: (a: string, b: string) => { - const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number) - const [aMajor, aMinor = 0, aPatch = 0] = parse(a) - const [bMajor, bMinor = 0, bPatch = 0] = parse(b) - if (aMajor !== bMajor) - return aMajor > bMajor ? 1 : -1 - if (aMinor !== bMinor) - return aMinor > bMinor ? 1 : -1 - if (aPatch !== bPatch) - return aPatch > bPatch ? 1 : -1 - return 0 - }, - getLatestVersion: (versions: string[]) => { - return versions.sort((a, b) => { - const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number) - const [aMaj, aMin = 0, aPat = 0] = parse(a) - const [bMaj, bMin = 0, bPat = 0] = parse(b) - if (aMaj !== bMaj) - return bMaj - aMaj - if (aMin !== bMin) - return bMin - aMin - return bPat - aPat - })[0] - }, -})) - const { useGitHubReleases, useGitHubUpload } = await import( '@/app/components/plugins/install-plugin/hooks', ) diff --git a/web/app/components/plugins/install-plugin/__tests__/hooks.spec.ts b/web/app/components/plugins/install-plugin/__tests__/hooks.spec.ts index b0e3ec5832..918a9b36e3 100644 --- a/web/app/components/plugins/install-plugin/__tests__/hooks.spec.ts +++ b/web/app/components/plugins/install-plugin/__tests__/hooks.spec.ts @@ -16,34 +16,6 @@ vi.mock('@/service/plugins', () => ({ uploadGitHub: (...args: unknown[]) => mockUploadGitHub(...args), })) -vi.mock('@/utils/semver', () => ({ - compareVersion: (a: string, b: string) => { - const parseVersion = (v: string) => v.replace(/^v/, '').split('.').map(Number) - const va = parseVersion(a) - const vb = parseVersion(b) - for (let i = 0; i < Math.max(va.length, vb.length); i++) { - const diff = (va[i] || 0) - (vb[i] || 0) - if (diff > 0) - return 1 - if (diff < 0) - return -1 - } - return 0 - }, - getLatestVersion: (versions: string[]) => { - return versions.sort((a, b) => { - const pa = a.replace(/^v/, '').split('.').map(Number) - const pb = b.replace(/^v/, '').split('.').map(Number) - for (let i = 0; i < Math.max(pa.length, pb.length); i++) { - const diff = (pa[i] || 0) - (pb[i] || 0) - if (diff !== 0) - return diff - } - return 0 - }).pop()! - }, -})) - const mockFetch = vi.fn() globalThis.fetch = mockFetch diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index 1e36daefc1..d37151a253 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -5,12 +5,12 @@ import { RiLoader2Line } from '@remixicon/react' import * as React from 'react' import { useEffect, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { gte } from 'semver' import Button from '@/app/components/base/button' import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' import { useAppContext } from '@/context/app-context' import { uninstallPlugin } from '@/service/plugins' import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plugins' +import { isEqualOrLaterThanVersion } from '@/utils/semver' import Card from '../../../card' import { TaskStatus } from '../../../types' import checkTaskStatus from '../../base/check-task-status' @@ -111,13 +111,13 @@ const Installed: FC = ({ const isDifyVersionCompatible = useMemo(() => { if (!langGeniusVersionInfo.current_version) return true - return gte(langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0') + return isEqualOrLaterThanVersion(langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0') }, [langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version]) return ( <>
-
+

{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}

= ({ />

{!isDifyVersionCompatible && ( -

+

{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: payload.meta.minimum_dify_version })}

)} diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index 275d4ca47b..8a4e0bd82a 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -5,11 +5,11 @@ import { RiLoader2Line } from '@remixicon/react' import * as React from 'react' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { gte } from 'semver' import Button from '@/app/components/base/button' import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' import { useAppContext } from '@/context/app-context' import { useInstallPackageFromMarketPlace, usePluginDeclarationFromMarketPlace, usePluginTaskList, useUpdatePackageFromMarketPlace } from '@/service/use-plugins' +import { isEqualOrLaterThanVersion } from '@/utils/semver' import Card from '../../../card' // import { RiInformation2Line } from '@remixicon/react' import { TaskStatus } from '../../../types' @@ -126,17 +126,17 @@ const Installed: FC = ({ const isDifyVersionCompatible = useMemo(() => { if (!pluginDeclaration || !langGeniusVersionInfo.current_version) return true - return gte(langGeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') + return isEqualOrLaterThanVersion(langGeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') }, [langGeniusVersionInfo.current_version, pluginDeclaration]) const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' }) return ( <>
-
+

{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}

{!isDifyVersionCompatible && ( -

+

{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}

)} diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 3f658c63a8..08da055bde 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -11,7 +11,6 @@ import { import * as React from 'react' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { gte } from 'semver' import Tooltip from '@/app/components/base/tooltip' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { API_PREFIX } from '@/config' @@ -20,6 +19,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context' import { useRenderI18nObject } from '@/hooks/use-i18n' import useTheme from '@/hooks/use-theme' import { cn } from '@/utils/classnames' +import { isEqualOrLaterThanVersion } from '@/utils/semver' import { getMarketplaceUrl } from '@/utils/var' import Badge from '../../base/badge' import { Github } from '../../base/icons/src/public/common' @@ -71,7 +71,7 @@ const PluginItem: FC = ({ const isDifyVersionCompatible = useMemo(() => { if (!langGeniusVersionInfo.current_version) return true - return gte(langGeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0') + return isEqualOrLaterThanVersion(langGeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0') }, [declarationMeta.minimum_dify_version, langGeniusVersionInfo.current_version]) const isDeprecated = useMemo(() => { @@ -164,8 +164,8 @@ const PluginItem: FC = ({ /> {category === PluginCategoryEnum.extension && ( <> -
·
-
+
·
+
= ({ && ( <> -
{t('from', { ns: 'plugin' })}
+
{t('from', { ns: 'plugin' })}
GitHub
@@ -196,7 +196,7 @@ const PluginItem: FC = ({ && ( <>
-
+
{t('from', { ns: 'plugin' })} {' '} marketplace @@ -210,7 +210,7 @@ const PluginItem: FC = ({ <>
-
Local Plugin
+
Local Plugin
)} @@ -219,14 +219,14 @@ const PluginItem: FC = ({ <>
-
Debugging Plugin
+
Debugging Plugin
)}
{/* Deprecated */} {source === PluginSource.marketplace && enable_marketplace && isDeprecated && ( -
+
· {t('deprecated', { ns: 'plugin' })} diff --git a/web/app/components/plugins/update-plugin/__tests__/index.spec.tsx b/web/app/components/plugins/update-plugin/__tests__/index.spec.tsx index 73fb132850..656bb042a4 100644 --- a/web/app/components/plugins/update-plugin/__tests__/index.spec.tsx +++ b/web/app/components/plugins/update-plugin/__tests__/index.spec.tsx @@ -104,20 +104,6 @@ vi.mock('../../install-plugin/install-from-github', () => ({ ), })) -// Mock semver -vi.mock('semver', () => ({ - lt: (v1: string, v2: string) => { - const parseVersion = (v: string) => v.split('.').map(Number) - const [major1, minor1, patch1] = parseVersion(v1) - const [major2, minor2, patch2] = parseVersion(v2) - if (major1 !== major2) - return major1 < major2 - if (minor1 !== minor2) - return minor1 < minor2 - return patch1 < patch2 - }, -})) - // ================================ // Test Data Factories // ================================ diff --git a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx index d662c2b6e0..9f14cd6c83 100644 --- a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx +++ b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx @@ -4,7 +4,6 @@ import type { Placement } from '@/app/components/base/ui/placement' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { lt } from 'semver' import Badge from '@/app/components/base/badge' import { Popover, @@ -14,6 +13,7 @@ import { import useTimestamp from '@/hooks/use-timestamp' import { useVersionListOfPlugin } from '@/service/use-plugins' import { cn } from '@/utils/classnames' +import { isEarlierThanVersion } from '@/utils/semver' type Props = { disabled?: boolean @@ -100,7 +100,7 @@ const PluginVersionPicker: FC = ({ onClick={() => handleSelect({ version: version.version, unique_identifier: version.unique_identifier, - isDowngrade: lt(version.version, currentVersion), + isDowngrade: isEarlierThanVersion(version.version, currentVersion), })} >
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index b613b64691..174b7a875c 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -4976,11 +4976,6 @@ "count": 1 } }, - "app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 2 - } - }, "app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 1 @@ -4997,11 +4992,6 @@ "count": 1 } }, - "app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx": { - "tailwindcss/enforce-consistent-class-order": { - "count": 2 - } - }, "app/components/plugins/marketplace/description/index.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 9 @@ -5480,9 +5470,6 @@ "no-restricted-imports": { "count": 1 }, - "tailwindcss/enforce-consistent-class-order": { - "count": 7 - }, "ts/no-explicit-any": { "count": 1 } diff --git a/web/package.json b/web/package.json index 663aac076c..e053c981e7 100644 --- a/web/package.json +++ b/web/package.json @@ -151,9 +151,9 @@ "remark-breaks": "4.0.0", "remark-directive": "4.0.0", "scheduler": "0.27.0", - "semver": "7.7.4", "sharp": "0.34.5", "sortablejs": "1.15.7", + "std-semver": "1.0.8", "streamdown": "2.5.0", "string-ts": "2.3.1", "tailwind-merge": "2.6.1", @@ -206,7 +206,6 @@ "@types/react-slider": "1.3.6", "@types/react-syntax-highlighter": "15.5.13", "@types/react-window": "1.8.8", - "@types/semver": "7.7.1", "@types/sortablejs": "1.15.9", "@typescript-eslint/parser": "8.57.1", "@typescript/native-preview": "7.0.0-dev.20260317.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index b8a67542a3..59bbea1f25 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -340,15 +340,15 @@ importers: scheduler: specifier: 0.27.0 version: 0.27.0 - semver: - specifier: 7.7.4 - version: 7.7.4 sharp: specifier: 0.34.5 version: 0.34.5 sortablejs: specifier: 1.15.7 version: 1.15.7 + std-semver: + specifier: 1.0.8 + version: 1.0.8 streamdown: specifier: 2.5.0 version: 2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -500,9 +500,6 @@ importers: '@types/react-window': specifier: 1.8.8 version: 1.8.8 - '@types/semver': - specifier: 7.7.1 - version: 7.7.1 '@types/sortablejs': specifier: 1.15.9 version: 1.15.9 @@ -3420,9 +3417,6 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/sortablejs@1.15.9': resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==} @@ -7115,6 +7109,10 @@ packages: std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + std-semver@1.0.8: + resolution: {integrity: sha512-9SN0XIjBBXCT6ZXXVnScJN4KP2RyFg6B8sEoFlugVHMANysfaEni4LTWlvUQQ/R0wgZl1Ovt9KBQbzn21kHoZA==} + engines: {node: '>=20.19.0'} + storybook@10.2.19: resolution: {integrity: sha512-UUm5eGSm6BLhkcFP0WbxkmAHJZfVN2ViLpIZOqiIPS++q32VYn+CLFC0lrTYTDqYvaG7i4BK4uowXYujzE4NdQ==} hasBin: true @@ -10755,8 +10753,6 @@ snapshots: '@types/resolve@1.20.6': {} - '@types/semver@7.7.1': {} - '@types/sortablejs@1.15.9': {} '@types/trusted-types@2.0.7': @@ -15205,6 +15201,8 @@ snapshots: std-env@4.0.0: {} + std-semver@1.0.8: {} + storybook@10.2.19(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@storybook/global': 5.0.0 diff --git a/web/utils/semver.spec.ts b/web/utils/semver.spec.ts index c2188a976c..42d6a3fb54 100644 --- a/web/utils/semver.spec.ts +++ b/web/utils/semver.spec.ts @@ -1,4 +1,4 @@ -import { compareVersion, getLatestVersion, isEqualOrLaterThanVersion } from './semver' +import { compareVersion, getLatestVersion, isEarlierThanVersion, isEqualOrLaterThanVersion } from './semver' describe('semver utilities', () => { describe('getLatestVersion', () => { @@ -72,4 +72,24 @@ describe('semver utilities', () => { expect(isEqualOrLaterThanVersion('1.0.0-alpha', '1.0.0')).toBe(false) }) }) + + describe('isEarlierThanVersion', () => { + it('should return true when baseVersion is less than targetVersion', () => { + expect(isEarlierThanVersion('1.0.0', '1.1.0')).toBe(true) + expect(isEarlierThanVersion('1.9.9', '2.0.0')).toBe(true) + expect(isEarlierThanVersion('1.0.0', '1.0.1')).toBe(true) + }) + + it('should return false when baseVersion is equal to or greater than targetVersion', () => { + expect(isEarlierThanVersion('1.0.0', '1.0.0')).toBe(false) + expect(isEarlierThanVersion('1.1.0', '1.0.0')).toBe(false) + expect(isEarlierThanVersion('1.0.1', '1.0.0')).toBe(false) + }) + + it('should handle pre-release versions correctly', () => { + expect(isEarlierThanVersion('1.0.0-beta', '1.0.0')).toBe(true) + expect(isEarlierThanVersion('1.0.0-alpha', '1.0.0-beta')).toBe(true) + expect(isEarlierThanVersion('1.0.0', '1.0.0-beta')).toBe(false) + }) + }) }) diff --git a/web/utils/semver.ts b/web/utils/semver.ts index aea84153ec..a22d219947 100644 --- a/web/utils/semver.ts +++ b/web/utils/semver.ts @@ -1,13 +1,19 @@ -import semver from 'semver' +import { compare, greaterOrEqual, lessThan, parse } from 'std-semver' export const getLatestVersion = (versionList: string[]) => { - return semver.rsort(versionList)[0] + return [...versionList].sort((versionA, versionB) => { + return compare(parse(versionB), parse(versionA)) + })[0] } export const compareVersion = (v1: string, v2: string) => { - return semver.compare(v1, v2) + return compare(parse(v1), parse(v2)) } export const isEqualOrLaterThanVersion = (baseVersion: string, targetVersion: string) => { - return semver.gte(baseVersion, targetVersion) + return greaterOrEqual(parse(baseVersion), parse(targetVersion)) +} + +export const isEarlierThanVersion = (baseVersion: string, targetVersion: string) => { + return lessThan(parse(baseVersion), parse(targetVersion)) }