Skip to content
Open
148 changes: 112 additions & 36 deletions app/components/Package/Dependencies.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { parseDependencyVersion } from '#shared/utils/npm'
import { SEVERITY_TEXT_COLORS, getHighestSeverity } from '#shared/utils/severity'
import { getOutdatedTooltip, getVersionClass } from '~/utils/npm/outdated-dependencies'

Expand Down Expand Up @@ -66,24 +67,41 @@ const sortedOptionalDependencies = computed(() => {
return Object.entries(props.optionalDependencies).sort(([a], [b]) => a.localeCompare(b))
})

// Get version tooltip
function getDepVersionTooltip(dep: string, version: string) {
const outdated = outdatedDeps.value[dep]
// Get version tooltip (key for outdated lookup, realName for vuln/replacement lookup)
function getDepVersionTooltip(key: string, realName: string, version: string) {
const outdated = outdatedDeps.value[key]
if (outdated) return getOutdatedTooltip(outdated, t)
if (getVulnerableDepInfo(dep) || getDeprecatedDepInfo(dep)) return version
if (replacementDeps.value[dep]) return t('package.dependencies.has_replacement')
if (getVulnerableDepInfo(realName) || getDeprecatedDepInfo(realName)) return version
if (replacementDeps.value[realName]) return t('package.dependencies.has_replacement')
return version
}

// Get version class
function getDepVersionClass(dep: string) {
const outdated = outdatedDeps.value[dep]
// Get version class (key for outdated lookup, realName for vuln/replacement lookup)
function getDepVersionClass(key: string, realName: string) {
const outdated = outdatedDeps.value[key]
if (outdated) return getVersionClass(outdated)
if (replacementDeps.value[dep]) return 'text-amber-700 dark:text-amber-500'
if (getVulnerableDepInfo(dep) || getDeprecatedDepInfo(dep)) return getVersionClass(undefined)
if (getVulnerableDepInfo(realName) || getDeprecatedDepInfo(realName))
return getVersionClass(undefined)
if (replacementDeps.value[realName]) return 'text-amber-700 dark:text-amber-500'
return getVersionClass(undefined)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Resolve npm: aliases — returns the real package name for links
function resolveDepName(key: string, value: string): string {
return parseDependencyVersion(value).name ?? key
}

// Resolve npm: aliases — returns the version range for display
function depRange(value: string): string {
return parseDependencyVersion(value).range ?? value
}

// Whether a dependency uses an npm: alias (listed name differs from the real package)
function isAliased(key: string, value: string): boolean {
const realName = parseDependencyVersion(value).name
return realName != null && realName !== key
}

const {
visibleItems: visibleDeps,
hasMore: hasMoreDeps,
Expand Down Expand Up @@ -127,10 +145,29 @@ const numberFormatter = useNumberFormatter()
:key="dep"
class="flex items-center justify-between py-1 text-sm gap-2"
>
<LinkBase :to="packageRoute(dep)" class="block truncate" dir="ltr">
<LinkBase
:to="packageRoute(resolveDepName(dep, version))"
class="block truncate"
dir="ltr"
>
{{ dep }}
</LinkBase>
<span class="flex items-center gap-1 max-w-[40%]" dir="ltr">
<TooltipApp
v-if="isAliased(dep, version)"
class="shrink-0 text-fg-muted"
:text="$t('package.dependencies.aliased_to', { name: resolveDepName(dep, version) })"
>
<button
type="button"
class="inline-flex items-center justify-center p-2 -m-2"
:aria-label="
$t('package.dependencies.aliased_to', { name: resolveDepName(dep, version) })
"
>
<span class="i-lucide:arrow-right-left w-3 h-3" aria-hidden="true" />
</button>
</TooltipApp>
<TooltipApp
v-if="outdatedDeps[dep]"
class="shrink-0"
Expand All @@ -146,7 +183,7 @@ const numberFormatter = useNumberFormatter()
</button>
</TooltipApp>
<TooltipApp
v-if="replacementDeps[dep]"
v-if="replacementDeps[resolveDepName(dep, version)]"
class="shrink-0 text-amber-700 dark:text-amber-500"
:text="$t('package.dependencies.has_replacement')"
>
Expand All @@ -159,41 +196,55 @@ const numberFormatter = useNumberFormatter()
</button>
</TooltipApp>
<LinkBase
v-if="getVulnerableDepInfo(dep)"
:to="packageRoute(dep, getVulnerableDepInfo(dep)!.version)"
v-if="getVulnerableDepInfo(resolveDepName(dep, version))"
:to="
packageRoute(
resolveDepName(dep, version),
getVulnerableDepInfo(resolveDepName(dep, version))!.version,
)
"
class="shrink-0"
:class="SEVERITY_TEXT_COLORS[getHighestSeverity(getVulnerableDepInfo(dep)!.counts)]"
:class="
SEVERITY_TEXT_COLORS[
getHighestSeverity(getVulnerableDepInfo(resolveDepName(dep, version))!.counts)
]
"
:aria-label="$t('package.dependencies.view_vulnerabilities')"
:title="
$t('package.dependencies.vulnerabilities_count', {
count: getVulnerableDepInfo(dep)!.counts.total,
count: getVulnerableDepInfo(resolveDepName(dep, version))!.counts.total,
})
"
classicon="i-lucide:shield-check"
/>
<LinkBase
v-if="getDeprecatedDepInfo(dep)"
:to="packageRoute(dep, getDeprecatedDepInfo(dep)!.version)"
v-if="getDeprecatedDepInfo(resolveDepName(dep, version))"
:to="
packageRoute(
resolveDepName(dep, version),
getDeprecatedDepInfo(resolveDepName(dep, version))!.version,
)
"
class="shrink-0 text-purple-700 dark:text-purple-500"
:aria-label="$t('package.deprecated.label')"
:title="getDeprecatedDepInfo(dep)!.message"
:title="getDeprecatedDepInfo(resolveDepName(dep, version))!.message"
classicon="i-lucide:octagon-alert"
/>
<LinkBase
:to="packageRoute(dep, version)"
:to="packageRoute(resolveDepName(dep, version), depRange(version))"
class="block truncate"
:class="getDepVersionClass(dep)"
:title="getDepVersionTooltip(dep, version)"
:class="getDepVersionClass(dep, resolveDepName(dep, version))"
:title="getDepVersionTooltip(dep, resolveDepName(dep, version), depRange(version))"
>
{{ version }}
{{ depRange(version) }}
</LinkBase>
<span v-if="outdatedDeps[dep]" class="sr-only">
({{ getOutdatedTooltip(outdatedDeps[dep], $t) }})
</span>
<span v-if="getVulnerableDepInfo(dep)" class="sr-only">
<span v-if="getVulnerableDepInfo(resolveDepName(dep, version))" class="sr-only">
({{
$t('package.dependencies.vulnerabilities_count', {
count: getVulnerableDepInfo(dep)!.counts.total,
count: getVulnerableDepInfo(resolveDepName(dep, version))!.counts.total,
})
}})
</span>
Expand Down Expand Up @@ -238,7 +289,11 @@ const numberFormatter = useNumberFormatter()
class="flex items-center justify-between py-1 text-sm gap-1 min-w-0"
>
<div class="flex items-center gap-2 min-w-0 flex-1">
<LinkBase :to="packageRoute(peer.name)" class="block min-w-0 break-all" dir="ltr">
<LinkBase
:to="packageRoute(resolveDepName(peer.name, peer.version))"
class="block min-w-0 break-all"
dir="ltr"
>
{{ peer.name }}
</LinkBase>
<TagStatic
Expand All @@ -250,12 +305,12 @@ const numberFormatter = useNumberFormatter()
</TagStatic>
</div>
<LinkBase
:to="packageRoute(peer.name, peer.version)"
:to="packageRoute(resolveDepName(peer.name, peer.version), depRange(peer.version))"
class="block truncate shrink-0 max-w-20"
:title="peer.version"
:title="depRange(peer.version)"
dir="ltr"
>
{{ peer.version }}
{{ depRange(peer.version) }}
</LinkBase>
</li>
</ul>
Expand Down Expand Up @@ -300,17 +355,38 @@ const numberFormatter = useNumberFormatter()
:key="dep"
class="flex items-baseline justify-between py-1 text-sm gap-2"
>
<LinkBase :to="packageRoute(dep)" class="block max-w-[80%] break-words" dir="ltr">
{{ dep }}
</LinkBase>
<LinkBase
:to="packageRoute(dep, version)"
class="block truncate"
:title="version"
:to="packageRoute(resolveDepName(dep, version))"
class="block max-w-[80%] break-words"
dir="ltr"
>
{{ version }}
{{ dep }}
</LinkBase>
<span class="flex items-center gap-1 min-w-0" dir="ltr">
<TooltipApp
v-if="isAliased(dep, version)"
class="shrink-0 text-fg-muted"
:text="$t('package.dependencies.aliased_to', { name: resolveDepName(dep, version) })"
>
<button
type="button"
class="inline-flex items-center justify-center p-2 -m-2"
:aria-label="
$t('package.dependencies.aliased_to', { name: resolveDepName(dep, version) })
"
>
<span class="i-lucide:arrow-right-left w-3 h-3" aria-hidden="true" />
</button>
</TooltipApp>
<LinkBase
:to="packageRoute(resolveDepName(dep, version), depRange(version))"
class="block truncate"
:title="depRange(version)"
dir="ltr"
>
{{ depRange(version) }}
</LinkBase>
</span>
</li>
</ul>
<button
Expand Down
24 changes: 14 additions & 10 deletions app/composables/npm/useOutdatedDependencies.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { PackageVersionsInfo } from 'fast-npm-meta'
import { getVersionsBatch } from 'fast-npm-meta'
import { difference, findMaxSatisfying, getMajor, getMinor, isGreater, isStable } from 'verkit'
import { parseDependencyVersion } from '#shared/utils/npm'
import {
type OutdatedDependencyInfo,
isNonSemverConstraint,
constraintIncludesPrerelease,
} from '~/utils/npm/outdated-dependencies'

Expand Down Expand Up @@ -67,16 +67,20 @@ export function useOutdatedDependencies(
return
}

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

if (semverEntries.length === 0) {
if (resolvedEntries.length === 0) {
outdated.value = {}
return
}

const packageNames = semverEntries.map(([name]) => name)
const packageNames = [...new Set(resolvedEntries.map(e => e.realName))]

const chunks: string[][] = []
for (let i = 0; i < packageNames.length; i += BATCH_SIZE) {
Expand All @@ -95,16 +99,16 @@ export function useOutdatedDependencies(
}

const results: Record<string, OutdatedDependencyInfo> = {}
for (const [name, constraint] of semverEntries) {
const data = versionMap.get(name)
for (const { key, realName, range } of resolvedEntries) {
const data = versionMap.get(realName)
if (!data) continue

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

const info = resolveOutdated(data.versions, latestTag, constraint)
const info = resolveOutdated(data.versions, latestTag, range)
if (info) {
results[name] = info
results[key] = info
}
}

Expand Down
7 changes: 6 additions & 1 deletion app/composables/npm/useReplacementDependencies.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements'
import { parseDependencyVersion } from '#shared/utils/npm'

async function fetchReplacements(
deps: Record<string, string>,
): Promise<Record<string, ModuleReplacement>> {
const names = Object.keys(deps)
const names = [
...new Set(
Object.entries(deps).map(([key, value]) => parseDependencyVersion(value).name ?? key),
),
]

const results = await Promise.all(
names.map(async name => {
Expand Down
15 changes: 0 additions & 15 deletions app/utils/npm/outdated-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,6 @@ export function constraintIncludesPrerelease(constraint: string): boolean {
)
}

/**
* Check if a constraint is a non-semver value (git URL, file path, etc.)
*/
export function isNonSemverConstraint(constraint: string): boolean {
return (
constraint.startsWith('git') ||
constraint.startsWith('http') ||
constraint.startsWith('file:') ||
constraint.startsWith('npm:') ||
constraint.startsWith('link:') ||
constraint.startsWith('workspace:') ||
constraint.includes('/')
)
}

/**
* Get tooltip text for an outdated dependency
*/
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,8 @@
"outdated_minor": "{count} Nebenversion zurück (neueste: {latest}) | {count} Nebenversionen zurück (neueste: {latest})",
"outdated_patch": "Patch-Update verfügbar (aktuellste: {latest})",
"has_replacement": "Für diese Abhängigkeit gibt es empfohlene Alternativen",
"vulnerabilities_count": "{count} Sicherheitslücke | {count} Sicherheitslücken"
"vulnerabilities_count": "{count} Sicherheitslücke | {count} Sicherheitslücken",
"aliased_to": "Alias für {name}"
},
"peer_dependencies": {
"title": "Peer-Abhängigkeit ({count}) | Peer-Abhängigkeiten ({count})",
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,8 @@
"outdated_minor": "{count} minor version behind (latest: {latest}) | {count} minor versions behind (latest: {latest})",
"outdated_patch": "Patch update available (latest: {latest})",
"has_replacement": "This dependency has suggested replacements",
"vulnerabilities_count": "{count} vulnerability | {count} vulnerabilities"
"vulnerabilities_count": "{count} vulnerability | {count} vulnerabilities",
"aliased_to": "Aliased to {name}"
},
"peer_dependencies": {
"title": "Peer Dependency ({count}) | Peer Dependencies ({count})",
Expand Down
3 changes: 3 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2064,6 +2064,9 @@
},
"vulnerabilities_count": {
"type": "string"
},
"aliased_to": {
"type": "string"
}
},
"additionalProperties": false
Expand Down
Loading
Loading