diff --git a/app/components/Package/Versions.vue b/app/components/Package/Versions.vue
index 2d621e9b74..1cc284335b 100644
--- a/app/components/Package/Versions.vue
+++ b/app/components/Package/Versions.vue
@@ -106,6 +106,14 @@ function versionRoute(version: string): RouteLocationRaw {
return packageRoute(props.packageName, version)
}
+// Route to the full versions history page
+const versionsPageRoute = computed((): RouteLocationRaw => {
+ const [org, name = ''] = props.packageName.startsWith('@')
+ ? props.packageName.split('/')
+ : ['', props.packageName]
+ return { name: 'package-versions', params: { org, name } }
+})
+
// Version to tags lookup (supports multiple tags per version)
const versionToTags = computed(() => buildVersionToTagsMap(props.distTags))
@@ -532,15 +540,26 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
id="versions"
>
-
- {{ $t('package.downloads.community_distribution') }}
-
+
+
+ View all versions
+
+
+ {{ $t('package.downloads.community_distribution') }}
+
+
diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue
index a57774f110..59f4b26558 100644
--- a/app/pages/package/[[org]]/[name].vue
+++ b/app/pages/package/[[org]]/[name].vue
@@ -272,6 +272,7 @@ const { diff: sizeDiff } = useInstallSizeDiff(packageName, resolvedVersion, pkg,
// → Preserve the server-rendered DOM, don't flash to skeleton.
const nuxtApp = useNuxtApp()
const route = useRoute()
+const isVersionsRoute = computed(() => route.name === 'package-versions')
const hasEmptyPayload =
import.meta.client &&
nuxtApp.payload.serverRendered &&
@@ -736,7 +737,8 @@ const showSkeleton = shallowRef(false)
-
+
+
Skeleton
-
+
+import type { PackageVersionInfo } from '#shared/types'
+import { compare } from 'semver'
+import { buildVersionToTagsMap, buildTaggedVersionRows } from '~/utils/versions'
+
+definePageMeta({
+ name: 'package-versions',
+})
+
+const route = useRoute()
+const router = useRouter()
+
+const packageName = computed(() => {
+ const { org, name } = route.params as { org?: string; name: string }
+ return org ? `${org}/${name}` : (name as string)
+})
+
+const orgName = computed(() => {
+ const name = packageName.value
+ if (!name.startsWith('@')) return null
+ const match = name.match(/^@([^/]+)\//)
+ return match ? match[1] : null
+})
+
+// ─── Mock data ────────────────────────────────────────────────────────────────
+// TODO: Replace distTags with pkg['dist-tags'] from usePackage()
+// TODO: Replace versionHistory with data from useAllPackageVersions()
+// TODO: Replace mockChangelogs with pre-rendered HTML from the server
+// (GitHub releases body or CHANGELOG.md, parsed server-side like README)
+
+const distTags: Record = {
+ latest: '3.4.21',
+ next: '3.5.0-beta.3',
+ beta: '3.5.0-beta.3',
+ rc: '3.5.0-rc.1',
+ alpha: '3.5.0-alpha.5',
+ csp: '3.4.21',
+ legacy: '2.7.16',
+}
+
+const versionHistory: PackageVersionInfo[] = [
+ { version: '3.5.0-beta.3', time: '2024-12-18T10:00:00Z', hasProvenance: true },
+ { version: '3.5.0-rc.1', time: '2024-12-10T10:00:00Z', hasProvenance: true },
+ { version: '3.5.0-alpha.5', time: '2024-11-28T10:00:00Z', hasProvenance: true },
+ { version: '3.5.0-alpha.4', time: '2024-11-10T10:00:00Z', hasProvenance: false },
+ { version: '3.5.0-alpha.3', time: '2024-10-22T10:00:00Z', hasProvenance: false },
+ { version: '3.4.21', time: '2024-12-05T10:00:00Z', hasProvenance: true },
+ { version: '3.4.20', time: '2024-11-20T10:00:00Z', hasProvenance: true },
+ { version: '3.4.19', time: '2024-11-08T10:00:00Z', hasProvenance: true },
+ { version: '3.4.18', time: '2024-10-25T10:00:00Z', hasProvenance: true },
+ { version: '3.4.17', time: '2024-10-01T10:00:00Z', hasProvenance: true },
+ { version: '3.4.0', time: '2024-02-15T10:00:00Z', hasProvenance: false },
+ { version: '3.3.13', time: '2024-01-10T10:00:00Z', hasProvenance: false },
+ { version: '3.3.0', time: '2023-05-11T10:00:00Z', hasProvenance: false },
+ { version: '3.2.47', time: '2023-03-30T10:00:00Z', hasProvenance: false },
+ { version: '3.0.0', time: '2022-09-29T10:00:00Z', hasProvenance: false },
+ { version: '2.7.16', time: '2023-12-08T10:00:00Z', hasProvenance: false },
+ { version: '2.7.15', time: '2023-09-12T10:00:00Z', hasProvenance: false },
+ { version: '2.7.14', time: '2023-06-01T10:00:00Z', hasProvenance: false },
+ { version: '2.7.0', time: '2022-07-01T10:00:00Z', hasProvenance: false },
+ { version: '2.6.14', time: '2022-03-14T10:00:00Z', hasProvenance: false },
+ {
+ version: '2.5.22',
+ time: '2018-03-20T10:00:00Z',
+ hasProvenance: false,
+ deprecated: 'Use vue@2.6.x or later',
+ },
+ {
+ version: '2.5.0',
+ time: '2017-10-13T10:00:00Z',
+ hasProvenance: false,
+ deprecated: 'Use vue@2.6.x or later',
+ },
+ { version: '1.0.28', time: '2016-12-15T10:00:00Z', hasProvenance: false },
+]
+
+// Changelog markdown strings keyed by version.
+// In production these would be pre-rendered HTML from the server
+// (e.g. GitHub release body or CHANGELOG.md, parsed like README).
+const mockChangelogs: Record = {
+ '3.5.0-beta.3': `### Bug Fixes
+
+- Fixed \`v-model\` not triggering update when used with custom modifier on the same component
+- Resolved hydration mismatch for \`\` with async setup components
+
+### Performance
+
+- Reduced scheduler flush cost for large component trees with many watchers`,
+
+ '3.5.0-rc.1': `## Vue 3.5 RC
+
+The API is now stable. This release candidate is intended for final testing before the stable release.
+
+### New Features
+
+- \`useTemplateRef()\` — reactive ref bound to a template element by string key
+- \`useId()\` — SSR-safe unique ID generation for accessibility attributes
+- Deferred \`\` — suspense boundary no longer blocks parent tree rendering
+
+### Breaking Changes
+
+> **Note:** These only affect experimental APIs that were previously behind flags.
+
+- Removed \`v-memo\` on component root nodes — use it on inner elements instead
+- \`defineModel()\` local mutation now requires explicit \`local\` option
+
+**Full Changelog**: [v3.4.21...v3.5.0-rc.1](https://github.com/vuejs/core/compare/v3.4.21...v3.5.0-rc.1)`,
+
+ '3.4.21': `### Bug Fixes
+
+- Fixed \`\` failing to restore scroll position on re-activation (#10156)
+- Corrected \`shallowReadonly\` not preserving array identity on nested access
+- Fixed compiler warning for \`v-bind\` shorthand used on \`\` elements
+
+**Full Changelog**: [v3.4.20...v3.4.21](https://github.com/vuejs/core/compare/v3.4.20...v3.4.21)`,
+
+ '3.4.0': `## Vue 3.4 — "Slam Dunk"
+
+### New Features
+
+- **Reactivity transform removed** — the experimental \`$ref\` sugar has been dropped; use \`ref()\` directly
+- **\`v-bind\` shorthand** — \`:foo\` can now be written as just \`:foo\` when binding a same-name prop
+- **\`defineModel()\` stable** — two-way binding macro is now stable and no longer requires opt-in
+- **Parser rewrite** — the template compiler's parser is 2× faster and produces better error messages
+
+### Breaking Changes
+
+- \`app.config.compilerOptions.isCustomElement\` now receives the full element tag with namespace prefix
+- \`@vue/reactivity\` no longer exports \`deferredComputed\` — use \`computed\` with a scheduler instead
+
+\`\`\`ts
+// Before
+const double = deferredComputed(() => count.value * 2)
+
+// After
+const double = computed(() => count.value * 2, { scheduler: queueMicrotask })
+\`\`\`
+
+**Full Changelog**: [v3.3.13...v3.4.0](https://github.com/vuejs/core/compare/v3.3.13...v3.4.0)`,
+
+ '3.0.0': `## Vue 3.0 — "One Piece"
+
+The first stable release of Vue 3. Rebuilt from the ground up with the Composition API, TypeScript, and a new reactivity system.
+
+### Highlights
+
+- **Composition API** — \`setup()\`, \`ref()\`, \`reactive()\`, \`computed()\`, \`watch()\`
+- **Fragments** — components can now have multiple root nodes
+- **Teleport** — render content in a different part of the DOM
+- **Suspense** — coordinate async dependency resolution in component trees
+- **Improved TypeScript support** — full type inference for component props and emits
+- **Tree-shakeable** — global APIs are now ES module exports
+
+### Migration
+
+Vue 3 is not backwards compatible with Vue 2. See the [Migration Guide](https://v3-migration.vuejs.org/) for a full list of breaking changes.
+
+**Full Changelog**: [github.com/vuejs/core](https://github.com/vuejs/core/blob/main/CHANGELOG.md)`,
+}
+
+// ─── Markdown rendering ───────────────────────────────────────────────────────
+// Minimal block-level markdown parser for changelog content.
+// Handles headings, lists, paragraphs, blockquotes, and inline formatting.
+// In production this would be replaced by server-rendered HTML (like README).
+
+function parseInline(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/\*\*(.+?)\*\*/g, '$1')
+ .replace(/`([^`]+)`/g, '$1')
+ .replace(
+ /\[([^\]]+)\]\((https?:[^)]+)\)/g,
+ '$1',
+ )
+}
+
+function parseChangelogMarkdown(markdown: string): string {
+ const lines = markdown.split('\n')
+ const out: string[] = []
+ let inList = false
+ let inBlockquote = false
+ let inCodeBlock = false
+ let codeLang = ''
+ let codeLines: string[] = []
+
+ const flushList = () => {
+ if (inList) {
+ out.push('')
+ inList = false
+ }
+ }
+ const flushBlockquote = () => {
+ if (inBlockquote) {
+ out.push('')
+ inBlockquote = false
+ }
+ }
+
+ for (const line of lines) {
+ // Code block fence
+ if (line.startsWith('```')) {
+ if (inCodeBlock) {
+ out.push(
+ `${codeLines.map(l => l.replace(/&/g, '&').replace(//g, '>')).join('\n')}
`,
+ )
+ inCodeBlock = false
+ codeLines = []
+ codeLang = ''
+ } else {
+ flushList()
+ flushBlockquote()
+ inCodeBlock = true
+ codeLang = line.slice(3).trim()
+ }
+ continue
+ }
+ if (inCodeBlock) {
+ codeLines.push(line)
+ continue
+ }
+
+ const trimmed = line.trim()
+
+ // Blank line
+ if (!trimmed) {
+ flushList()
+ flushBlockquote()
+ continue
+ }
+
+ // Blockquote
+ if (trimmed.startsWith('> ')) {
+ flushList()
+ if (!inBlockquote) {
+ out.push('')
+ inBlockquote = true
+ }
+ out.push(`${parseInline(trimmed.slice(2))}
`)
+ continue
+ }
+ flushBlockquote()
+
+ // Headings
+ const h2 = trimmed.match(/^## (.+)/)
+ const h3 = trimmed.match(/^### (.+)/)
+ if (h2) {
+ flushList()
+ out.push(`${parseInline(h2[1]!)}
`)
+ continue
+ }
+ if (h3) {
+ flushList()
+ out.push(`${parseInline(h3[1]!)}
`)
+ continue
+ }
+
+ // List item
+ const li = trimmed.match(/^[-*] (.+)/)
+ if (li) {
+ if (!inList) {
+ out.push('')
+ inList = true
+ }
+ out.push(`- ${parseInline(li[1]!)}
`)
+ continue
+ }
+
+ // Paragraph
+ flushList()
+ out.push(`${parseInline(trimmed)}
`)
+ }
+
+ flushList()
+ flushBlockquote()
+ return out.join('\n')
+}
+
+// ─── Derived data ─────────────────────────────────────────────────────────────
+
+const versionToTagsMap = computed(() => buildVersionToTagsMap(distTags))
+
+const sortedVersions = computed(() =>
+ [...versionHistory]
+ .sort((a, b) => compare(b.version, a.version))
+ .map(v => ({
+ ...v,
+ tags: versionToTagsMap.value.get(v.version),
+ hasChangelog: v.version in mockChangelogs,
+ })),
+)
+
+const tagRows = computed(() => buildTaggedVersionRows(distTags))
+
+function getVersionTime(version: string): string | undefined {
+ return versionHistory.find(v => v.version === version)?.time
+}
+
+// ─── Changelog side panel ─────────────────────────────────────────────────────
+
+const selectedChangelogVersion = ref(null)
+
+const selectedChangelogHtml = computed(() => {
+ if (!selectedChangelogVersion.value) return ''
+ const raw = mockChangelogs[selectedChangelogVersion.value]
+ return raw ? parseChangelogMarkdown(raw) : ''
+})
+
+function toggleChangelog(version: string) {
+ selectedChangelogVersion.value = selectedChangelogVersion.value === version ? null : version
+}
+
+// ─── Jump to version ──────────────────────────────────────────────────────────
+
+const jumpVersion = ref('')
+const jumpError = ref('')
+
+function navigateToVersion() {
+ const v = jumpVersion.value.trim()
+ if (!v) return
+ if (!versionHistory.some(entry => entry.version === v)) {
+ jumpError.value = `"${v}" not found`
+ return
+ }
+ jumpError.value = ''
+ router.push(packageRoute(packageName.value, v))
+}
+
+watch(jumpVersion, () => {
+ jumpError.value = ''
+})
+
+
+
+
+
+
+
+
+
+
+
+
+ Current Tags
+
+
+
+
+
+
+
+ latest
+ {{ tag }}
+
+
{{ tagRows[0].version }}
+
+
+
+
+
+
+
+
+
+
+ {{ tag }}
+
+
+
+
+ {{ row.version }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Version History
+
+ ({{ sortedVersions.length }})
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ v.version }}
+
+
+
+ {{ tag }}
+
+
+
+ deprecated
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ v{{ selectedChangelogVersion }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+