From da26851d1ef6f24932431b1fee6e49579c891563 Mon Sep 17 00:00:00 2001 From: Atriiy Date: Sun, 26 Apr 2026 09:15:40 +0800 Subject: [PATCH 01/10] feat: add toggle buttons to control prerelease & deprecated versions --- app/pages/package/[[org]]/[name]/versions.vue | 217 ++++++++++++++---- app/utils/versions.ts | 11 +- i18n/locales/en.json | 11 +- i18n/schema.json | 27 +++ 4 files changed, 225 insertions(+), 41 deletions(-) diff --git a/app/pages/package/[[org]]/[name]/versions.vue b/app/pages/package/[[org]]/[name]/versions.vue index 2f91330670..d25e32aeb9 100644 --- a/app/pages/package/[[org]]/[name]/versions.vue +++ b/app/pages/package/[[org]]/[name]/versions.vue @@ -10,7 +10,9 @@ import { filterVersions, getVersionGroupKey, getVersionGroupLabel, + isPrereleaseVersion, } from '~/utils/versions' +import type { TaggedVersionRow } from '~/utils/versions' import { fetchAllPackageVersions } from '~/utils/npm/api' definePageMeta({ @@ -143,10 +145,37 @@ const versionToTagsMap = computed(() => buildVersionToTagsMap(distTags.value)) const tagRows = computed(() => buildTaggedVersionRows(distTags.value)) const latestTagRow = computed(() => tagRows.value.find(r => r.tags.includes('latest')) ?? null) + +const otherTagRowsAll = computed(() => tagRows.value.filter(r => !r.tags.includes('latest'))) +const stableOtherTagRows = computed(() => + otherTagRowsAll.value.filter(r => !isPrereleaseVersion(r.version)), +) +const hiddenPrereleaseTagCount = computed( + () => otherTagRowsAll.value.length - stableOtherTagRows.value.length, +) + +function sortTagRows(rows: TaggedVersionRow[]): TaggedVersionRow[] { + if (tagsSortMode.value === 'date') { + const dir = tagsSortOrder.value === 'desc' ? 1 : -1 + return [...rows].sort((rowA, rowB) => { + const timeA = versionTimes.value[rowA.version] ?? '' + const timeB = versionTimes.value[rowB.version] ?? '' + return dir * timeB.localeCompare(timeA) + }) + } + return [...rows].sort((rowA, rowB) => compareTagRows(rowA, rowB, versionTimes.value)) +} + +function selectTagsSort(mode: 'priority' | 'date') { + if (tagsSortMode.value === mode && mode === 'date') { + tagsSortOrder.value = tagsSortOrder.value === 'desc' ? 'asc' : 'desc' + return + } + tagsSortMode.value = mode +} + const otherTagRows = computed(() => - tagRows.value - .filter(r => !r.tags.includes('latest')) - .sort((rowA, rowB) => compareTagRows(rowA, rowB, versionTimes.value)), + sortTagRows(showHiddenTags.value ? otherTagRowsAll.value : stableOtherTagRows.value), ) function getVersionTime(version: string): string | undefined { @@ -202,6 +231,29 @@ watch( { immediate: true }, ) +// ─── View toggles ───────────────────────────────────────────────────────────── + +const showPrereleases = ref(false) +const showDeprecated = ref(false) +const tagsSortMode = ref<'priority' | 'date'>('priority') +const tagsSortOrder = ref<'asc' | 'desc'>('desc') +const showHiddenTags = ref(false) + +const visibleVersionGroups = computed(() => { + if (showPrereleases.value && showDeprecated.value) return versionGroups.value + return versionGroups.value + .map(group => + Object.assign({}, group, { + versions: group.versions.filter(v => { + if (!showPrereleases.value && isPrereleaseVersion(v)) return false + if (!showDeprecated.value && fullVersionMap.value?.get(v)?.deprecated) return false + return true + }), + }), + ) + .filter(group => group.versions.length > 0) +}) + // ─── Version filter ─────────────────────────────────────────────────────────── const versionFilterInput = ref('') @@ -224,8 +276,8 @@ const filteredVersionSet = computed(() => { }) const filteredGroups = computed(() => { - if (!isFilterActive.value || !filteredVersionSet.value) return versionGroups.value - return versionGroups.value + if (!isFilterActive.value || !filteredVersionSet.value) return visibleVersionGroups.value + return visibleVersionGroups.value .map(group => Object.assign({}, group, { versions: group.versions.filter(v => filteredVersionSet.value!.has(v)), @@ -286,39 +338,71 @@ const flatItems = computed(() => { /

{{ $t('package.versions.page_title') }}

-
- - +
- - + + + + + - +
+
+ + + + + + +
@@ -327,9 +411,45 @@ const flatItems = computed(() => {
-

- {{ $t('package.versions.current_tags') }} -

+
+

+ {{ $t('package.versions.current_tags') }} +

+
+ + +
+
(() => {
+ + + diff --git a/app/utils/versions.ts b/app/utils/versions.ts index e2aa8741a7..8cd93f95a6 100644 --- a/app/utils/versions.ts +++ b/app/utils/versions.ts @@ -1,4 +1,4 @@ -import { compare, satisfies, validRange, valid } from 'semver' +import { compare, prerelease, satisfies, validRange, valid } from 'semver' /** * Utilities for handling npm package versions and dist-tags @@ -39,6 +39,15 @@ export function parseVersion(version: string): ParsedVersion { } } +/** + * Check if a version is a pre-release (has a `-` suffix per semver). + * @param version - The version string (e.g., "1.0.0-beta.1", "2.0.0") + * @returns true if the version has a prerelease component + */ +export function isPrereleaseVersion(version: string): boolean { + return prerelease(version) !== null +} + /** * Extract the prerelease channel from a version string * @param version - The version string (e.g., "1.0.0-beta.1") diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 6851ed5511..25f676b9a3 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -570,7 +570,16 @@ }, "page_title": "Version History", "current_tags": "Current Tags", - "no_match_filter": "No versions match {filter}" + "no_match_filter": "No versions match {filter}", + "show_prereleases": "Show pre-releases", + "show_deprecated": "Show deprecated", + "sort_tags_by_priority": "Sort by tag", + "sort_tags_by_date": "Sort by date", + "sort_tags_by_date_asc": "Sort by date, oldest first", + "sort_tags_by_date_desc": "Sort by date, newest first", + "tags_hidden": "{count} pre-release tag hidden | {count} pre-release tags hidden", + "show_all_tags": "Show all", + "hide_prerelease_tags": "Hide pre-release tags" }, "timeline": { "load_more": "Load more", diff --git a/i18n/schema.json b/i18n/schema.json index 7b6a3b3945..4f6d1970e7 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -1716,6 +1716,33 @@ }, "no_match_filter": { "type": "string" + }, + "show_prereleases": { + "type": "string" + }, + "show_deprecated": { + "type": "string" + }, + "sort_tags_by_priority": { + "type": "string" + }, + "sort_tags_by_date": { + "type": "string" + }, + "sort_tags_by_date_asc": { + "type": "string" + }, + "sort_tags_by_date_desc": { + "type": "string" + }, + "tags_hidden": { + "type": "string" + }, + "show_all_tags": { + "type": "string" + }, + "hide_prerelease_tags": { + "type": "string" } }, "additionalProperties": false From 79f4834ef17b4b4312b12db858838412e948c3f2 Mon Sep 17 00:00:00 2001 From: Atriiy Date: Sun, 26 Apr 2026 09:24:35 +0800 Subject: [PATCH 02/10] perf: avoid unnecessary process --- app/pages/package/[[org]]/[name]/versions.vue | 21 +++++++++---------- i18n/locales/en.json | 1 + i18n/schema.json | 3 +++ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/pages/package/[[org]]/[name]/versions.vue b/app/pages/package/[[org]]/[name]/versions.vue index d25e32aeb9..50b77500e4 100644 --- a/app/pages/package/[[org]]/[name]/versions.vue +++ b/app/pages/package/[[org]]/[name]/versions.vue @@ -160,7 +160,7 @@ function sortTagRows(rows: TaggedVersionRow[]): TaggedVersionRow[] { return [...rows].sort((rowA, rowB) => { const timeA = versionTimes.value[rowA.version] ?? '' const timeB = versionTimes.value[rowB.version] ?? '' - return dir * timeB.localeCompare(timeA) + return dir * (timeB < timeA ? -1 : timeB > timeA ? 1 : 0) }) } return [...rows].sort((rowA, rowB) => compareTagRows(rowA, rowB, versionTimes.value)) @@ -242,15 +242,14 @@ const showHiddenTags = ref(false) const visibleVersionGroups = computed(() => { if (showPrereleases.value && showDeprecated.value) return versionGroups.value return versionGroups.value - .map(group => - Object.assign({}, group, { - versions: group.versions.filter(v => { - if (!showPrereleases.value && isPrereleaseVersion(v)) return false - if (!showDeprecated.value && fullVersionMap.value?.get(v)?.deprecated) return false - return true - }), - }), - ) + .map(group => { + const versions = group.versions.filter(v => { + if (!showPrereleases.value && isPrereleaseVersion(v)) return false + if (!showDeprecated.value && fullVersionMap.value?.get(v)?.deprecated) return false + return true + }) + return versions.length === group.versions.length ? group : { ...group, versions } + }) .filter(group => group.versions.length > 0) }) @@ -342,7 +341,7 @@ const flatItems = computed(() => {
-
- +
+ - - - +
+ + +
+
+
Date: Sun, 26 Apr 2026 16:12:51 +0800 Subject: [PATCH 04/10] refactor: use pre-defined tailwind token --- app/pages/package/[[org]]/[name]/versions.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/package/[[org]]/[name]/versions.vue b/app/pages/package/[[org]]/[name]/versions.vue index 5ddd669084..8d3a9c084d 100644 --- a/app/pages/package/[[org]]/[name]/versions.vue +++ b/app/pages/package/[[org]]/[name]/versions.vue @@ -376,7 +376,7 @@ const flatItems = computed(() => {
-
+
+ +
+ + +
+
+
+ + + {{ row.primaryVersion.version }} + + +
+ + +
+
+
+ + {{ tag }} + +
+
+
+
+