Skip to content

Commit 2263784

Browse files
authored
feat: support resolving tag and version ranges (#103)
1 parent da9e55d commit 2263784

4 files changed

Lines changed: 77 additions & 16 deletions

File tree

app/components/PackageDependencies.vue

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,14 @@ const sortedOptionalDependencies = computed(() => {
9191
>
9292
<span class="i-carbon-warning-alt w-3 h-3" />
9393
</span>
94-
<span
94+
<NuxtLink
95+
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
9596
class="font-mono text-xs text-right truncate"
9697
:class="getVersionClass(outdatedDeps[dep])"
9798
:title="outdatedDeps[dep] ? getOutdatedTooltip(outdatedDeps[dep]) : version"
9899
>
99100
{{ version }}
100-
</span>
101+
</NuxtLink>
101102
<span v-if="outdatedDeps[dep]" class="sr-only">
102103
({{ getOutdatedTooltip(outdatedDeps[dep]) }})
103104
</span>
@@ -143,12 +144,16 @@ const sortedOptionalDependencies = computed(() => {
143144
optional
144145
</span>
145146
</div>
146-
<span
147+
<NuxtLink
148+
:to="{
149+
name: 'package',
150+
params: { package: [...peer.name.split('/'), 'v', peer.version] },
151+
}"
147152
class="font-mono text-xs text-fg-subtle max-w-[40%] text-right truncate"
148153
:title="peer.version"
149154
>
150155
{{ peer.version }}
151-
</span>
156+
</NuxtLink>
152157
</li>
153158
</ul>
154159
<button
@@ -187,12 +192,13 @@ const sortedOptionalDependencies = computed(() => {
187192
>
188193
{{ dep }}
189194
</NuxtLink>
190-
<span
195+
<NuxtLink
196+
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
191197
class="font-mono text-xs text-fg-subtle max-w-[50%] text-right truncate"
192198
:title="version"
193199
>
194200
{{ version }}
195-
</span>
201+
</NuxtLink>
196202
</li>
197203
</ul>
198204
<button

app/composables/useNpmRegistry.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
} from '#shared/types'
1111
import type { ReleaseType } from 'semver'
1212
import { maxSatisfying, prerelease, major, minor, diff, gt } from 'semver'
13-
import { compareVersions } from '~/utils/versions'
13+
import { compareVersions, isExactVersion } from '~/utils/versions'
1414

1515
const NPM_REGISTRY = 'https://registry.npmjs.org'
1616
const NPM_API = 'https://api.npmjs.org'
@@ -154,11 +154,40 @@ export function usePackage(
154154
name: MaybeRefOrGetter<string>,
155155
requestedVersion?: MaybeRefOrGetter<string | null>,
156156
) {
157-
return useLazyAsyncData(
157+
const asyncData = useLazyAsyncData(
158158
() => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`,
159159
() =>
160160
fetchNpmPackage(toValue(name)).then(r => transformPackument(r, toValue(requestedVersion))),
161161
)
162+
163+
// Resolve requestedVersion to an exact version
164+
// Handles: exact versions, dist-tags (latest, next), and semver ranges (^4.2, >=1.0.0)
165+
const resolvedVersion = computed(() => {
166+
const pkg = asyncData.data.value
167+
const reqVer = toValue(requestedVersion)
168+
if (!pkg || !reqVer) return null
169+
170+
// 1. Check if it's already an exact version in pkg.versions
171+
if (isExactVersion(reqVer) && pkg.versions[reqVer]) {
172+
return reqVer
173+
}
174+
175+
// 2. Check if it's a dist-tag (latest, next, beta, etc.)
176+
const tagVersion = pkg['dist-tags']?.[reqVer]
177+
if (tagVersion) {
178+
return tagVersion
179+
}
180+
181+
// 3. Try to resolve as a semver range
182+
const versions = Object.keys(pkg.versions)
183+
const resolved = maxSatisfying(versions, reqVer)
184+
return resolved
185+
})
186+
187+
return {
188+
...asyncData,
189+
resolvedVersion,
190+
}
162191
}
163192

164193
export function usePackageDownloads(

app/pages/[...package].vue

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const orgName = computed(() => {
5050
return match ? match[1] : null
5151
})
5252
53-
const { data: pkg, status, error } = usePackage(packageName, requestedVersion)
53+
const { data: pkg, status, error, resolvedVersion } = usePackage(packageName, requestedVersion)
5454
5555
const { data: downloads } = usePackageDownloads(packageName, 'last-week')
5656
const { data: weeklyDownloads } = usePackageWeeklyDownloadEvolution(packageName, { weeks: 52 })
@@ -109,15 +109,16 @@ const sizeTooltip = computed(() => {
109109
return chunks.filter(Boolean).join('\n')
110110
})
111111
112-
// Get the version to display (requested or latest)
112+
// Get the version to display (resolved version or latest)
113113
const displayVersion = computed(() => {
114114
if (!pkg.value) return null
115115
116-
const reqVer = requestedVersion.value
117-
if (reqVer && pkg.value.versions[reqVer]) {
118-
return pkg.value.versions[reqVer]
116+
// Use resolved version if available
117+
if (resolvedVersion.value) {
118+
return pkg.value.versions[resolvedVersion.value] ?? null
119119
}
120120
121+
// Fallback to latest
121122
const latestTag = pkg.value['dist-tags']?.latest
122123
if (!latestTag) return null
123124
return pkg.value.versions[latestTag] ?? null
@@ -331,21 +332,33 @@ defineOgImageComponent('Package', {
331332
v-if="displayVersion"
332333
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
333334
>
335+
<!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
336+
<template v-if="resolvedVersion !== requestedVersion">
337+
<span class="font-mono text-fg-muted text-sm">{{ requestedVersion }}</span>
338+
<span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" />
339+
</template>
340+
341+
<NuxtLink
342+
v-if="resolvedVersion !== requestedVersion"
343+
:to="`/${pkg.name}/v/${displayVersion.version}`"
344+
title="View permalink for this version"
345+
>{{ displayVersion.version }}</NuxtLink
346+
>
347+
<span v-else>v{{ displayVersion.version }}</span>
348+
334349
<a
335350
v-if="hasProvenance(displayVersion)"
336351
:href="`https://www.npmjs.com/package/${pkg.name}/v/${displayVersion.version}#provenance`"
337352
target="_blank"
338353
rel="noopener noreferrer"
339-
class="inline-flex items-center gap-1.5 text-fg-muted hover:text-fg-muted/80 transition-colors duration-200"
354+
class="inline-flex items-center gap-1.5 text-fg-muted hover:text-fg transition-colors duration-200"
340355
title="Verified provenance"
341356
>
342-
v{{ displayVersion.version }}
343357
<span
344358
class="i-solar-shield-check-outline w-3.5 h-3.5 shrink-0"
345359
aria-hidden="true"
346360
/>
347361
</a>
348-
<span v-else>v{{ displayVersion.version }}</span>
349362
<span
350363
v-if="
351364
requestedVersion &&

app/utils/versions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
import { valid } from 'semver'
2+
13
/**
24
* Utilities for handling npm package versions and dist-tags
35
*/
46

7+
/**
8+
* Check if a version string is an exact semver version.
9+
* Returns true for "1.2.3", "1.0.0-beta.1", etc.
10+
* Returns false for ranges like "^1.2.3", ">=1.0.0", tags like "latest", etc.
11+
* @param version - The version string to check
12+
* @returns true if the version is an exact semver version
13+
*/
14+
export function isExactVersion(version: string): boolean {
15+
return valid(version) !== null
16+
}
17+
518
/** Parsed semver version components */
619
export interface ParsedVersion {
720
major: number

0 commit comments

Comments
 (0)