Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions app/components/PackageDependencies.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,14 @@ const sortedOptionalDependencies = computed(() => {
>
<span class="i-carbon-warning-alt w-3 h-3" />
</span>
<span
<NuxtLink
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
class="font-mono text-xs text-right truncate"
:class="getVersionClass(outdatedDeps[dep])"
:title="outdatedDeps[dep] ? getOutdatedTooltip(outdatedDeps[dep]) : version"
>
{{ version }}
</span>
</NuxtLink>
<span v-if="outdatedDeps[dep]" class="sr-only">
({{ getOutdatedTooltip(outdatedDeps[dep]) }})
</span>
Expand Down Expand Up @@ -143,12 +144,16 @@ const sortedOptionalDependencies = computed(() => {
optional
</span>
</div>
<span
<NuxtLink
:to="{
name: 'package',
params: { package: [...peer.name.split('/'), 'v', peer.version] },
}"
class="font-mono text-xs text-fg-subtle max-w-[40%] text-right truncate"
:title="peer.version"
>
{{ peer.version }}
</span>
</NuxtLink>
</li>
</ul>
<button
Expand Down Expand Up @@ -187,12 +192,13 @@ const sortedOptionalDependencies = computed(() => {
>
{{ dep }}
</NuxtLink>
<span
<NuxtLink
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
class="font-mono text-xs text-fg-subtle max-w-[50%] text-right truncate"
:title="version"
>
{{ version }}
</span>
</NuxtLink>
</li>
</ul>
<button
Expand Down
33 changes: 31 additions & 2 deletions app/composables/useNpmRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
} from '#shared/types'
import type { ReleaseType } from 'semver'
import { maxSatisfying, prerelease, major, minor, diff, gt } from 'semver'
import { compareVersions } from '~/utils/versions'
import { compareVersions, isExactVersion } from '~/utils/versions'

const NPM_REGISTRY = 'https://registry.npmjs.org'
const NPM_API = 'https://api.npmjs.org'
Expand Down Expand Up @@ -154,11 +154,40 @@ export function usePackage(
name: MaybeRefOrGetter<string>,
requestedVersion?: MaybeRefOrGetter<string | null>,
) {
return useLazyAsyncData(
const asyncData = useLazyAsyncData(
() => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`,
() =>
fetchNpmPackage(toValue(name)).then(r => transformPackument(r, toValue(requestedVersion))),
)

// Resolve requestedVersion to an exact version
// Handles: exact versions, dist-tags (latest, next), and semver ranges (^4.2, >=1.0.0)
const resolvedVersion = computed(() => {
const pkg = asyncData.data.value
const reqVer = toValue(requestedVersion)
if (!pkg || !reqVer) return null

// 1. Check if it's already an exact version in pkg.versions
if (isExactVersion(reqVer) && pkg.versions[reqVer]) {
return reqVer
}

// 2. Check if it's a dist-tag (latest, next, beta, etc.)
const tagVersion = pkg['dist-tags']?.[reqVer]
if (tagVersion) {
return tagVersion
}

// 3. Try to resolve as a semver range
const versions = Object.keys(pkg.versions)
const resolved = maxSatisfying(versions, reqVer)
return resolved
})

return {
...asyncData,
resolvedVersion,
}
}

export function usePackageDownloads(
Expand Down
29 changes: 21 additions & 8 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const orgName = computed(() => {
return match ? match[1] : null
})

const { data: pkg, status, error } = usePackage(packageName, requestedVersion)
const { data: pkg, status, error, resolvedVersion } = usePackage(packageName, requestedVersion)

const { data: downloads } = usePackageDownloads(packageName, 'last-week')
const { data: weeklyDownloads } = usePackageWeeklyDownloadEvolution(packageName, { weeks: 52 })
Expand Down Expand Up @@ -109,15 +109,16 @@ const sizeTooltip = computed(() => {
return chunks.filter(Boolean).join('\n')
})

// Get the version to display (requested or latest)
// Get the version to display (resolved version or latest)
const displayVersion = computed(() => {
if (!pkg.value) return null

const reqVer = requestedVersion.value
if (reqVer && pkg.value.versions[reqVer]) {
return pkg.value.versions[reqVer]
// Use resolved version if available
if (resolvedVersion.value) {
return pkg.value.versions[resolvedVersion.value] ?? null
}

// Fallback to latest
const latestTag = pkg.value['dist-tags']?.latest
if (!latestTag) return null
return pkg.value.versions[latestTag] ?? null
Expand Down Expand Up @@ -317,21 +318,33 @@ defineOgImageComponent('Package', {
v-if="displayVersion"
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
>
<!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
<template v-if="resolvedVersion !== requestedVersion">
<span class="font-mono text-fg-muted text-sm">{{ requestedVersion }}</span>
<span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" />
</template>

<NuxtLink
v-if="resolvedVersion !== requestedVersion"
:to="`/${pkg.name}/v/${displayVersion.version}`"
title="View permalink for this version"
>{{ displayVersion.version }}</NuxtLink
>
<span v-else>v{{ displayVersion.version }}</span>

<a
v-if="hasProvenance(displayVersion)"
:href="`https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 text-fg-muted hover:text-fg-muted/80 transition-colors duration-200"
class="inline-flex items-center gap-1.5 text-fg-muted hover:text-fg transition-colors duration-200"
title="Verified provenance"
>
v{{ displayVersion.version }}
<span
class="i-solar-shield-check-outline w-3.5 h-3.5 shrink-0"
aria-hidden="true"
/>
</a>
<span v-else>v{{ displayVersion.version }}</span>
<span
v-if="
requestedVersion &&
Expand Down
13 changes: 13 additions & 0 deletions app/utils/versions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { valid } from 'semver'

/**
* Utilities for handling npm package versions and dist-tags
*/

/**
* Check if a version string is an exact semver version.
* Returns true for "1.2.3", "1.0.0-beta.1", etc.
* Returns false for ranges like "^1.2.3", ">=1.0.0", tags like "latest", etc.
* @param version - The version string to check
* @returns true if the version is an exact semver version
*/
export function isExactVersion(version: string): boolean {
return valid(version) !== null
}

/** Parsed semver version components */
export interface ParsedVersion {
major: number
Expand Down