From 32b21b0ae9ee32da52410f7a0c2d7959abb6dfac Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Mon, 3 Aug 2026 17:36:57 +0800 Subject: [PATCH 1/2] feat: add deprecated marking and time gap info to package timeline page - Add `deprecated` field to TimelineVersion API type and response - Show deprecated badge and message on the latest deprecated version only - Render deprecated message using the same sub-event layout (dot + left border) - Add time gap sub-event showing duration between consecutive releases (>7 days), computed from raw ms diff, independent of semver predecessor - Use plural-choice-aware $t() calls for duration formatting; fix 360-364 day boundary returning "0 years" - Add i18n keys (en + zh-CN/zh-TW) for deprecated label and time gap formatting --- .../[[org]]/[packageName].vue | 112 ++++++++++++++---- i18n/locales/en.json | 5 + i18n/locales/zh-CN.json | 7 +- i18n/locales/zh-TW.json | 7 +- i18n/schema.json | 15 +++ server/api/registry/timeline/[...pkg].get.ts | 2 + 6 files changed, 124 insertions(+), 24 deletions(-) diff --git a/app/pages/package-timeline/[[org]]/[packageName].vue b/app/pages/package-timeline/[[org]]/[packageName].vue index a86c3f2af3..ce92993cc9 100644 --- a/app/pages/package-timeline/[[org]]/[packageName].vue +++ b/app/pages/package-timeline/[[org]]/[packageName].vue @@ -174,6 +174,19 @@ if (import.meta.client) { const bytesFormatter = useBytesFormatter() +// Format a duration (in days) into a localized human-readable string +function formatDuration(days: number): string { + if (days < 30) return $t('package.timeline.time_gap_days', { count: days }, days) + if (days < 365) + return $t( + 'package.timeline.time_gap_months', + { count: Math.floor(days / 30) }, + Math.floor(days / 30), + ) + const years = Math.floor(days / 365) + return $t('package.timeline.time_gap_years', { count: years }, years) +} + // Detect notable changes between consecutive versions (size, license, ESM, types) // Versions are compared against their semver predecessor, not chronological neighbor, // so interleaved legacy releases don't produce misleading cross-line diffs. @@ -189,11 +202,32 @@ const versionSubEvents = computed(() => { } for (const current of entries) { - const previous = prevBySemver.get(current.version) - if (!previous) continue - const events: SubEvent[] = [] + // Time gap from the previous chronological release + // entries are sorted newest-first by publish time, so index+1 is the predecessor + const chronologicalIndex = entries.indexOf(current) + const prevRelease = entries[chronologicalIndex + 1] + if (prevRelease) { + const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000 + const gapMs = Date.parse(current.time) - Date.parse(prevRelease.time) + if (gapMs > SEVEN_DAYS_MS) { + const gapDays = Math.floor(gapMs / (1000 * 60 * 60 * 24)) + events.push({ + key: 'timeGap', + positive: false, + icon: 'i-lucide:clock', + text: $t('package.timeline.time_gap', { duration: formatDuration(gapDays) }), + }) + } + } + + const previous = prevBySemver.get(current.version) + if (!previous) { + if (events.length) result.set(current.version, events) + continue + } + // Size changes const currentSize = sizeCache.get(sizeKey(current.version)) const previousSize = sizeCache.get(sizeKey(previous.version)) @@ -332,6 +366,13 @@ const versionSubEvents = computed(() => { return result }) +// The latest deprecated version — the first deprecated entry by publish time. +// npm registry doesn't record a separate deprecation timestamp, so the badge +// is shown once on the newest deprecated release only. +const latestDeprecatedVersion = computed( + () => timelineEntries.value.find(e => e.deprecated) ?? null, +) + const selectedVersion = shallowRef(null) useSeoMeta({ @@ -375,7 +416,7 @@ useSeoMeta({ :class="entry.version === version ? 'bg-accent border-accent' : 'bg-bg-subtle'" /> -
+
{{ tag }} + +
    + +
  1. -
diff --git a/i18n/locales/en.json b/i18n/locales/en.json index a09e22b59f..71db183eec 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -646,6 +646,11 @@ "trusted_publisher_removed": "Trusted publishing removed", "provenance_added": "Provenance enabled", "provenance_removed": "Provenance removed", + "deprecated": "Deprecated", + "time_gap": "{duration} after previous release", + "time_gap_days": "{count} day | {count} days", + "time_gap_months": "{count} month | {count} months", + "time_gap_years": "{count} year | {count} years", "chart": { "tab_aria_label": "Metric selection", "dependency_size": "Dependency Size", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 99d9019d6a..a0e9391946 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -601,7 +601,12 @@ "version_events": "版本 {version}:{events}", "general_description": "折线图展示了 {package} 包从版本 {first} 到 {last} 的 {metric} 变化情况。版本 {first} 的 {metric} 为 {first_value},版本 {last} 为 {last_value}(整体变化 {overall_progress_percentage}%)。{key_changes} {watermark}。" } - } + }, + "deprecated": "已弃用", + "time_gap": "距上一版本 {duration}", + "time_gap_days": "{count} 天", + "time_gap_months": "{count} 个月", + "time_gap_years": "{count} 年" }, "dependencies": { "title": "依赖({count} 个)", diff --git a/i18n/locales/zh-TW.json b/i18n/locales/zh-TW.json index e2b15eefb7..f5ac51ecec 100644 --- a/i18n/locales/zh-TW.json +++ b/i18n/locales/zh-TW.json @@ -555,7 +555,12 @@ "timeline": { "chart": { "copy_alt": {} - } + }, + "deprecated": "已棄用", + "time_gap": "距上一版本 {duration}", + "time_gap_days": "{count} 天", + "time_gap_months": "{count} 個月", + "time_gap_years": "{count} 年" }, "dependencies": { "title": "相依({count} 個)", diff --git a/i18n/schema.json b/i18n/schema.json index b7793e498c..98e54c83b7 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -1942,6 +1942,21 @@ "provenance_removed": { "type": "string" }, + "deprecated": { + "type": "string" + }, + "time_gap": { + "type": "string" + }, + "time_gap_days": { + "type": "string" + }, + "time_gap_months": { + "type": "string" + }, + "time_gap_years": { + "type": "string" + }, "chart": { "type": "object", "properties": { diff --git a/server/api/registry/timeline/[...pkg].get.ts b/server/api/registry/timeline/[...pkg].get.ts index cd399ef3c8..b12594465c 100644 --- a/server/api/registry/timeline/[...pkg].get.ts +++ b/server/api/registry/timeline/[...pkg].get.ts @@ -11,6 +11,7 @@ export interface TimelineVersion { hasTypes?: boolean hasTrustedPublisher?: boolean hasProvenance?: boolean + deprecated?: string tags: string[] } @@ -79,6 +80,7 @@ export default defineCachedEventHandler( // oxlint-disable-next-line eslint/no-underscore-dangle hasTrustedPublisher: version._npmUser?.trustedPublisher ? true : undefined, hasProvenance: version.dist?.attestations ? true : undefined, + deprecated: version.deprecated || undefined, tags: tagsByVersion.get(v) ?? [], } }) From 1c574bc9456b6ee443563fae2903e34d290bd8ab Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Tue, 4 Aug 2026 21:27:56 +0800 Subject: [PATCH 2/2] fix: update --- .../[[org]]/[packageName].vue | 72 +++++-------------- i18n/locales/en.json | 1 - i18n/locales/zh-CN.json | 1 - i18n/locales/zh-TW.json | 1 - i18n/schema.json | 3 - server/api/registry/timeline/[...pkg].get.ts | 2 - 6 files changed, 19 insertions(+), 61 deletions(-) diff --git a/app/pages/package-timeline/[[org]]/[packageName].vue b/app/pages/package-timeline/[[org]]/[packageName].vue index ce92993cc9..701428e161 100644 --- a/app/pages/package-timeline/[[org]]/[packageName].vue +++ b/app/pages/package-timeline/[[org]]/[packageName].vue @@ -366,13 +366,6 @@ const versionSubEvents = computed(() => { return result }) -// The latest deprecated version — the first deprecated entry by publish time. -// npm registry doesn't record a separate deprecation timestamp, so the badge -// is shown once on the newest deprecated release only. -const latestDeprecatedVersion = computed( - () => timelineEntries.value.find(e => e.deprecated) ?? null, -) - const selectedVersion = shallowRef(null) useSeoMeta({ @@ -416,7 +409,7 @@ useSeoMeta({ :class="entry.version === version ? 'bg-accent border-accent' : 'bg-bg-subtle'" /> -
+
{{ tag }} - -
    - -
  1. -
diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 71db183eec..f99d2286e8 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -646,7 +646,6 @@ "trusted_publisher_removed": "Trusted publishing removed", "provenance_added": "Provenance enabled", "provenance_removed": "Provenance removed", - "deprecated": "Deprecated", "time_gap": "{duration} after previous release", "time_gap_days": "{count} day | {count} days", "time_gap_months": "{count} month | {count} months", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index a0e9391946..16b44a37ac 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -602,7 +602,6 @@ "general_description": "折线图展示了 {package} 包从版本 {first} 到 {last} 的 {metric} 变化情况。版本 {first} 的 {metric} 为 {first_value},版本 {last} 为 {last_value}(整体变化 {overall_progress_percentage}%)。{key_changes} {watermark}。" } }, - "deprecated": "已弃用", "time_gap": "距上一版本 {duration}", "time_gap_days": "{count} 天", "time_gap_months": "{count} 个月", diff --git a/i18n/locales/zh-TW.json b/i18n/locales/zh-TW.json index f5ac51ecec..a5670617a3 100644 --- a/i18n/locales/zh-TW.json +++ b/i18n/locales/zh-TW.json @@ -556,7 +556,6 @@ "chart": { "copy_alt": {} }, - "deprecated": "已棄用", "time_gap": "距上一版本 {duration}", "time_gap_days": "{count} 天", "time_gap_months": "{count} 個月", diff --git a/i18n/schema.json b/i18n/schema.json index 98e54c83b7..d5b0bb6623 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -1942,9 +1942,6 @@ "provenance_removed": { "type": "string" }, - "deprecated": { - "type": "string" - }, "time_gap": { "type": "string" }, diff --git a/server/api/registry/timeline/[...pkg].get.ts b/server/api/registry/timeline/[...pkg].get.ts index b12594465c..cd399ef3c8 100644 --- a/server/api/registry/timeline/[...pkg].get.ts +++ b/server/api/registry/timeline/[...pkg].get.ts @@ -11,7 +11,6 @@ export interface TimelineVersion { hasTypes?: boolean hasTrustedPublisher?: boolean hasProvenance?: boolean - deprecated?: string tags: string[] } @@ -80,7 +79,6 @@ export default defineCachedEventHandler( // oxlint-disable-next-line eslint/no-underscore-dangle hasTrustedPublisher: version._npmUser?.trustedPublisher ? true : undefined, hasProvenance: version.dist?.attestations ? true : undefined, - deprecated: version.deprecated || undefined, tags: tagsByVersion.get(v) ?? [], } })