Skip to content
Closed
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
40 changes: 34 additions & 6 deletions app/components/Package/Dependencies.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { SEVERITY_TEXT_COLORS, getHighestSeverity } from '#shared/utils/severity'
import { getOutdatedTooltip, getVersionClass } from '~/utils/npm/outdated-dependencies'
import { parseNpmAlias } from '~/utils/npm/alias'

const { t } = useI18n()

Expand Down Expand Up @@ -114,7 +115,11 @@ 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(parseNpmAlias(version)?.name ?? dep)"
class="block truncate"
dir="ltr"
>
{{ dep }}
</LinkBase>
<span class="flex items-center gap-1 max-w-[40%]" dir="ltr">
Expand Down Expand Up @@ -169,7 +174,12 @@ const numberFormatter = useNumberFormatter()
<span class="sr-only">{{ $t('package.deprecated.label') }}</span>
</LinkBase>
<LinkBase
:to="packageRoute(dep, version)"
:to="
packageRoute(
parseNpmAlias(version)?.name ?? dep,
parseNpmAlias(version)?.range ?? version,
)
"
class="block truncate"
:class="getDepVersionClass(dep)"
:title="getDepVersionTooltip(dep, version)"
Expand Down Expand Up @@ -227,15 +237,24 @@ 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 max-w-[70%] break-words" dir="ltr">
<LinkBase
:to="packageRoute(parseNpmAlias(peer.version)?.name ?? peer.name)"
class="block max-w-[70%] break-words"
dir="ltr"
>
{{ peer.name }}
</LinkBase>
<TagStatic v-if="peer.optional" :title="$t('package.dependencies.optional')">
{{ $t('package.dependencies.optional') }}
</TagStatic>
</div>
<LinkBase
:to="packageRoute(peer.name, peer.version)"
:to="
packageRoute(
parseNpmAlias(peer.version)?.name ?? peer.name,
parseNpmAlias(peer.version)?.range ?? peer.version,
)
"
class="block truncate max-w-[30%]"
:title="peer.version"
dir="ltr"
Expand Down Expand Up @@ -288,11 +307,20 @@ 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">
<LinkBase
:to="packageRoute(parseNpmAlias(version)?.name ?? dep)"
class="block max-w-[80%] break-words"
dir="ltr"
>
{{ dep }}
</LinkBase>
<LinkBase
:to="packageRoute(dep, version)"
:to="
packageRoute(
parseNpmAlias(version)?.name ?? dep,
parseNpmAlias(version)?.range ?? version,
)
"
class="block truncate"
:title="version"
dir="ltr"
Expand Down
18 changes: 18 additions & 0 deletions app/utils/npm/alias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Parses npm alias syntax: "npm:real-pkg@^1.0.0"
* Returns { name, range } for the real package, or null if not an alias.
*
* @example
* parseNpmAlias('npm:real-pkg@^1.0.0') // { name: 'real-pkg', range: '^1.0.0' }
* parseNpmAlias('npm:@scope/pkg@^1.0.0') // { name: '@scope/pkg', range: '^1.0.0' }
* parseNpmAlias('npm:real-pkg') // { name: 'real-pkg', range: '' }
* parseNpmAlias('^1.0.0') // null
*/
export function parseNpmAlias(version: string): { name: string; range: string } | null {
if (!version.startsWith('npm:')) return null
const spec = version.slice(4) // strip 'npm:'
// Handle scoped packages like @scope/pkg@1.0.0 β€” find @ after position 0
const atIdx = spec.startsWith('@') ? spec.indexOf('@', 1) : spec.indexOf('@')
if (atIdx === -1) return { name: spec, range: '' }
return { name: spec.slice(0, atIdx), range: spec.slice(atIdx + 1) }
Comment on lines +11 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Return null for empty alias targets.

npm: currently parses to { name: '', range: '' }, which would send callers such as Dependencies.vue down the alias path with an empty package name instead of letting them fall back cleanly. Please treat blank specs as invalid and add a regression case for it.

🩹 Suggested guard
 export function parseNpmAlias(version: string): { name: string; range: string } | null {
   if (!version.startsWith('npm:')) return null
   const spec = version.slice(4) // strip 'npm:'
+  if (!spec.trim()) return null
   // Handle scoped packages like `@scope/pkg`@1.0.0 β€” find @ after position 0
   const atIdx = spec.startsWith('@') ? spec.indexOf('@', 1) : spec.indexOf('@')
   if (atIdx === -1) return { name: spec, range: '' }
   return { name: spec.slice(0, atIdx), range: spec.slice(atIdx + 1) }
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function parseNpmAlias(version: string): { name: string; range: string } | null {
if (!version.startsWith('npm:')) return null
const spec = version.slice(4) // strip 'npm:'
// Handle scoped packages like @scope/pkg@1.0.0 β€” find @ after position 0
const atIdx = spec.startsWith('@') ? spec.indexOf('@', 1) : spec.indexOf('@')
if (atIdx === -1) return { name: spec, range: '' }
return { name: spec.slice(0, atIdx), range: spec.slice(atIdx + 1) }
export function parseNpmAlias(version: string): { name: string; range: string } | null {
if (!version.startsWith('npm:')) return null
const spec = version.slice(4) // strip 'npm:'
if (!spec.trim()) return null
// Handle scoped packages like `@scope/pkg`@1.0.0 β€” find @ after position 0
const atIdx = spec.startsWith('@') ? spec.indexOf('@', 1) : spec.indexOf('@')
if (atIdx === -1) return { name: spec, range: '' }
return { name: spec.slice(0, atIdx), range: spec.slice(atIdx + 1) }
}

}
62 changes: 62 additions & 0 deletions test/unit/app/utils/npm/alias.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest'
import { parseNpmAlias } from '~/utils/npm/alias'

describe('parseNpmAlias', () => {
it('returns null for a regular semver range (not an alias)', () => {
expect(parseNpmAlias('^1.0.0')).toBeNull()
})

it('returns null for an empty string', () => {
expect(parseNpmAlias('')).toBeNull()
})

it('returns null for a plain version number', () => {
expect(parseNpmAlias('1.2.3')).toBeNull()
})

it('returns null for a workspace protocol', () => {
expect(parseNpmAlias('workspace:*')).toBeNull()
})

it('parses a simple alias with version range', () => {
expect(parseNpmAlias('npm:real-pkg@^1.0.0')).toEqual({
name: 'real-pkg',
range: '^1.0.0',
})
})

it('parses a scoped package alias', () => {
expect(parseNpmAlias('npm:@scope/pkg@^1.0.0')).toEqual({
name: '@scope/pkg',
range: '^1.0.0',
})
})

it('parses an alias without a version range', () => {
expect(parseNpmAlias('npm:real-pkg')).toEqual({
name: 'real-pkg',
range: '',
})
})

it('parses an alias with an exact version', () => {
expect(parseNpmAlias('npm:real-pkg@2.0.0')).toEqual({
name: 'real-pkg',
range: '2.0.0',
})
})

it('parses a scoped package alias without version', () => {
expect(parseNpmAlias('npm:@scope/pkg')).toEqual({
name: '@scope/pkg',
range: '',
})
})

it('strips the npm: prefix correctly', () => {
const result = parseNpmAlias('npm:lodash@^4.0.0')
expect(result).not.toBeNull()
expect(result!.name).toBe('lodash')
expect(result!.name).not.toContain('npm:')
})
})
Loading