Skip to content

Commit 6ca0a8a

Browse files
committed
fix: show and link aliased deps correctly
1 parent 7f2fc1a commit 6ca0a8a

3 files changed

Lines changed: 100 additions & 46 deletions

File tree

app/components/Package/Dependencies.vue

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { SEVERITY_TEXT_COLORS, getHighestSeverity } from '#shared/utils/severity'
3-
import { getOutdatedTooltip, getVersionClass } from '~/utils/npm/outdated-dependencies'
3+
import { getOutdatedTooltip, getVersionClass, parseDepValue } from '~/utils/npm/outdated-dependencies'
44
55
const { t } = useI18n()
66
@@ -71,24 +71,34 @@ const sortedOptionalDependencies = computed(() => {
7171
return Object.entries(props.optionalDependencies).sort(([a], [b]) => a.localeCompare(b))
7272
})
7373
74-
// Get version tooltip
75-
function getDepVersionTooltip(dep: string, version: string) {
76-
const outdated = outdatedDeps.value[dep]
74+
// Get version tooltip (key for outdated lookup, realName for vuln/replacement lookup)
75+
function getDepVersionTooltip(key: string, realName: string, version: string) {
76+
const outdated = outdatedDeps.value[key]
7777
if (outdated) return getOutdatedTooltip(outdated, t)
78-
if (getVulnerableDepInfo(dep) || getDeprecatedDepInfo(dep)) return version
79-
if (replacementDeps.value[dep]) return t('package.dependencies.has_replacement')
78+
if (getVulnerableDepInfo(realName) || getDeprecatedDepInfo(realName)) return version
79+
if (replacementDeps.value[realName]) return t('package.dependencies.has_replacement')
8080
return version
8181
}
8282
83-
// Get version class
84-
function getDepVersionClass(dep: string) {
85-
const outdated = outdatedDeps.value[dep]
83+
// Get version class (key for outdated lookup, realName for vuln/replacement lookup)
84+
function getDepVersionClass(key: string, realName: string) {
85+
const outdated = outdatedDeps.value[key]
8686
if (outdated) return getVersionClass(outdated)
87-
if (getVulnerableDepInfo(dep) || getDeprecatedDepInfo(dep)) return getVersionClass(undefined)
88-
if (replacementDeps.value[dep]) return 'text-amber-700 dark:text-amber-500'
87+
if (getVulnerableDepInfo(realName) || getDeprecatedDepInfo(realName)) return getVersionClass(undefined)
88+
if (replacementDeps.value[realName]) return 'text-amber-700 dark:text-amber-500'
8989
return getVersionClass(undefined)
9090
}
9191
92+
// Resolve npm: aliases — returns the real package name for links
93+
function depName(key: string, value: string): string {
94+
return parseDepValue(value).name ?? key
95+
}
96+
97+
// Resolve npm: aliases — returns the version range for display
98+
function depRange(value: string): string {
99+
return parseDepValue(value).range ?? value
100+
}
101+
92102
const numberFormatter = useNumberFormatter()
93103
</script>
94104

@@ -114,7 +124,7 @@ const numberFormatter = useNumberFormatter()
114124
:key="dep"
115125
class="flex items-center justify-between py-1 text-sm gap-2"
116126
>
117-
<LinkBase :to="packageRoute(dep)" class="block truncate" dir="ltr">
127+
<LinkBase :to="packageRoute(depName(dep, version))" class="block truncate" dir="ltr">
118128
{{ dep }}
119129
</LinkBase>
120130
<span class="flex items-center gap-1 max-w-[40%]" dir="ltr">
@@ -133,7 +143,7 @@ const numberFormatter = useNumberFormatter()
133143
</button>
134144
</TooltipApp>
135145
<TooltipApp
136-
v-if="replacementDeps[dep]"
146+
v-if="replacementDeps[depName(dep, version)]"
137147
class="shrink-0 text-amber-700 dark:text-amber-500"
138148
:text="$t('package.dependencies.has_replacement')"
139149
>
@@ -146,43 +156,43 @@ const numberFormatter = useNumberFormatter()
146156
</button>
147157
</TooltipApp>
148158
<LinkBase
149-
v-if="getVulnerableDepInfo(dep)"
150-
:to="packageRoute(dep, getVulnerableDepInfo(dep)!.version)"
159+
v-if="getVulnerableDepInfo(depName(dep, version))"
160+
:to="packageRoute(depName(dep, version), getVulnerableDepInfo(depName(dep, version))!.version)"
151161
class="shrink-0"
152-
:class="SEVERITY_TEXT_COLORS[getHighestSeverity(getVulnerableDepInfo(dep)!.counts)]"
162+
:class="SEVERITY_TEXT_COLORS[getHighestSeverity(getVulnerableDepInfo(depName(dep, version))!.counts)]"
153163
:title="
154164
$t('package.dependencies.vulnerabilities_count', {
155-
count: getVulnerableDepInfo(dep)!.counts.total,
165+
count: getVulnerableDepInfo(depName(dep, version))!.counts.total,
156166
})
157167
"
158168
classicon="i-lucide:shield-check"
159169
>
160170
<span class="sr-only">{{ $t('package.dependencies.view_vulnerabilities') }}</span>
161171
</LinkBase>
162172
<LinkBase
163-
v-if="getDeprecatedDepInfo(dep)"
164-
:to="packageRoute(dep, getDeprecatedDepInfo(dep)!.version)"
173+
v-if="getDeprecatedDepInfo(depName(dep, version))"
174+
:to="packageRoute(depName(dep, version), getDeprecatedDepInfo(depName(dep, version))!.version)"
165175
class="shrink-0 text-purple-700 dark:text-purple-500"
166-
:title="getDeprecatedDepInfo(dep)!.message"
176+
:title="getDeprecatedDepInfo(depName(dep, version))!.message"
167177
classicon="i-lucide:octagon-alert"
168178
>
169179
<span class="sr-only">{{ $t('package.deprecated.label') }}</span>
170180
</LinkBase>
171181
<LinkBase
172-
:to="packageRoute(dep, version)"
182+
:to="packageRoute(depName(dep, version), depRange(version))"
173183
class="block truncate"
174-
:class="getDepVersionClass(dep)"
175-
:title="getDepVersionTooltip(dep, version)"
184+
:class="getDepVersionClass(dep, depName(dep, version))"
185+
:title="getDepVersionTooltip(dep, depName(dep, version), depRange(version))"
176186
>
177-
{{ version }}
187+
{{ depRange(version) }}
178188
</LinkBase>
179189
<span v-if="outdatedDeps[dep]" class="sr-only">
180190
({{ getOutdatedTooltip(outdatedDeps[dep], $t) }})
181191
</span>
182-
<span v-if="getVulnerableDepInfo(dep)" class="sr-only">
192+
<span v-if="getVulnerableDepInfo(depName(dep, version))" class="sr-only">
183193
({{
184194
$t('package.dependencies.vulnerabilities_count', {
185-
count: getVulnerableDepInfo(dep)!.counts.total,
195+
count: getVulnerableDepInfo(depName(dep, version))!.counts.total,
186196
})
187197
}})
188198
</span>
@@ -227,20 +237,20 @@ const numberFormatter = useNumberFormatter()
227237
class="flex items-center justify-between py-1 text-sm gap-1 min-w-0"
228238
>
229239
<div class="flex items-center gap-2 min-w-0 flex-1">
230-
<LinkBase :to="packageRoute(peer.name)" class="block max-w-[70%] break-words" dir="ltr">
240+
<LinkBase :to="packageRoute(depName(peer.name, peer.version))" class="block max-w-[70%] break-words" dir="ltr">
231241
{{ peer.name }}
232242
</LinkBase>
233243
<TagStatic v-if="peer.optional" :title="$t('package.dependencies.optional')">
234244
{{ $t('package.dependencies.optional') }}
235245
</TagStatic>
236246
</div>
237247
<LinkBase
238-
:to="packageRoute(peer.name, peer.version)"
248+
:to="packageRoute(depName(peer.name, peer.version), depRange(peer.version))"
239249
class="block truncate max-w-[30%]"
240-
:title="peer.version"
250+
:title="depRange(peer.version)"
241251
dir="ltr"
242252
>
243-
{{ peer.version }}
253+
{{ depRange(peer.version) }}
244254
</LinkBase>
245255
</li>
246256
</ul>
@@ -288,16 +298,16 @@ const numberFormatter = useNumberFormatter()
288298
:key="dep"
289299
class="flex items-baseline justify-between py-1 text-sm gap-2"
290300
>
291-
<LinkBase :to="packageRoute(dep)" class="block max-w-[80%] break-words" dir="ltr">
301+
<LinkBase :to="packageRoute(depName(dep, version))" class="block max-w-[80%] break-words" dir="ltr">
292302
{{ dep }}
293303
</LinkBase>
294304
<LinkBase
295-
:to="packageRoute(dep, version)"
305+
:to="packageRoute(depName(dep, version), depRange(version))"
296306
class="block truncate"
297-
:title="version"
307+
:title="depRange(version)"
298308
dir="ltr"
299309
>
300-
{{ version }}
310+
{{ depRange(version) }}
301311
</LinkBase>
302312
</li>
303313
</ul>

app/composables/npm/useOutdatedDependencies.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { getVersionsBatch } from 'fast-npm-meta'
33
import { maxSatisfying, prerelease, major, minor, diff, gt } from 'semver'
44
import {
55
type OutdatedDependencyInfo,
6-
isNonSemverConstraint,
76
constraintIncludesPrerelease,
7+
parseDepValue,
88
} from '~/utils/npm/outdated-dependencies'
99

1010
const BATCH_SIZE = 50
@@ -67,16 +67,18 @@ export function useOutdatedDependencies(
6767
return
6868
}
6969

70-
const semverEntries = Object.entries(deps).filter(
71-
([, constraint]) => !isNonSemverConstraint(constraint),
72-
)
70+
// Resolve npm: aliases and filter out non-semver constraints
71+
const resolvedEntries = Object.entries(deps).map(([key, value]) => {
72+
const parsed = parseDepValue(value)
73+
return { key, realName: parsed.name ?? key, range: parsed.range }
74+
}).filter((e): e is typeof e & { range: string } => e.range !== null)
7375

74-
if (semverEntries.length === 0) {
76+
if (resolvedEntries.length === 0) {
7577
outdated.value = {}
7678
return
7779
}
7880

79-
const packageNames = semverEntries.map(([name]) => name)
81+
const packageNames = [...new Set(resolvedEntries.map(e => e.realName))]
8082

8183
const chunks: string[][] = []
8284
for (let i = 0; i < packageNames.length; i += BATCH_SIZE) {
@@ -95,16 +97,16 @@ export function useOutdatedDependencies(
9597
}
9698

9799
const results: Record<string, OutdatedDependencyInfo> = {}
98-
for (const [name, constraint] of semverEntries) {
99-
const data = versionMap.get(name)
100+
for (const { key, realName, range } of resolvedEntries) {
101+
const data = versionMap.get(realName)
100102
if (!data) continue
101103

102104
const latestTag = data.distTags.latest
103105
if (!latestTag) continue
104106

105-
const info = resolveOutdated(data.versions, latestTag, constraint)
107+
const info = resolveOutdated(data.versions, latestTag, range)
106108
if (info) {
107-
results[name] = info
109+
results[key] = info
108110
}
109111
}
110112

app/utils/npm/outdated-dependencies.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,57 @@ export function constraintIncludesPrerelease(constraint: string): boolean {
2525
)
2626
}
2727

28+
/** Parsed result of an npm dependency value */
29+
export interface ParsedDepValue {
30+
/** The real package name (different from key only for aliases) */
31+
name: string | null
32+
/** The semver range or version, null for non-resolvable values (file:, git, etc.) */
33+
range: string | null
34+
}
35+
36+
/**
37+
* Parse a dependency value which may be a semver range, an npm alias, or a non-semver reference.
38+
*
39+
* Examples:
40+
* "^4.2.0" { name: null, range: "^4.2.0" }
41+
* "npm:string-width@^4.2.0" { name: "string-width", range: "^4.2.0" }
42+
* "npm:@scope/pkg@^1.0.0" { name: "@scope/pkg", range: "^1.0.0" }
43+
* "file:../foo" { name: null, range: null }
44+
*/
45+
export function parseDepValue(value: string): ParsedDepValue {
46+
if (value.startsWith('npm:')) {
47+
const aliasBody = value.slice(4) // strip "npm:"
48+
// Scoped: @scope/name@range
49+
if (aliasBody.startsWith('@')) {
50+
const secondAt = aliasBody.indexOf('@', 1)
51+
if (secondAt !== -1) {
52+
return { name: aliasBody.slice(0, secondAt), range: aliasBody.slice(secondAt + 1) }
53+
}
54+
return { name: aliasBody, range: null }
55+
}
56+
// Unscoped: name@range
57+
const atIndex = aliasBody.indexOf('@')
58+
if (atIndex !== -1) {
59+
return { name: aliasBody.slice(0, atIndex), range: aliasBody.slice(atIndex + 1) }
60+
}
61+
return { name: aliasBody, range: null }
62+
}
63+
64+
if (isNonSemverConstraint(value)) {
65+
return { name: null, range: null }
66+
}
67+
68+
return { name: null, range: value }
69+
}
70+
2871
/**
2972
* Check if a constraint is a non-semver value (git URL, file path, etc.)
3073
*/
31-
export function isNonSemverConstraint(constraint: string): boolean {
74+
function isNonSemverConstraint(constraint: string): boolean {
3275
return (
3376
constraint.startsWith('git') ||
3477
constraint.startsWith('http') ||
3578
constraint.startsWith('file:') ||
36-
constraint.startsWith('npm:') ||
3779
constraint.startsWith('link:') ||
3880
constraint.startsWith('workspace:') ||
3981
constraint.includes('/')

0 commit comments

Comments
 (0)