Skip to content

Commit a45ad65

Browse files
committed
fix: route dependency version ranges to pkg page with semver filter instead of 404
Dependency links with version ranges (e.g. "^18.0.0 || ^19.0.0", ">15 <=16.0.2") previously navigated to a non-existent version page and 404'd. Now `packageRoute()` distinguishes exact versions from ranges: exact versions link to the version page, while ranges link to the package page with `?semver=<range>#versions`, pre-populating the existing "Filter by semver" input. Closes #1120
1 parent 526c5db commit a45ad65

3 files changed

Lines changed: 144 additions & 6 deletions

File tree

app/components/Package/Versions.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ const effectiveCurrentVersion = computed(
105105
() => props.selectedVersion ?? props.distTags.latest ?? undefined,
106106
)
107107
108-
// Semver range filter
109-
const semverFilter = ref('')
108+
// Semver range filter (initialized from ?semver= query param if present)
109+
const semverFilter = ref((typeof route.query.semver === 'string' ? route.query.semver : '') || '')
110110
111111
// Load all versions when a valid semver filter is entered
112112
watch(semverFilter, async newFilter => {

app/utils/router.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { RouteLocationRaw } from 'vue-router'
2+
import { valid as isValidSingleVersion } from 'semver'
23
import { splitPackageName } from '~/utils/package-name'
34

45
export function packageRoute(
@@ -9,15 +10,28 @@ export function packageRoute(
910
const { org, name } = splitPackageName(packageName)
1011

1112
if (version) {
13+
if (isValidSingleVersion(version)) {
14+
return {
15+
name: 'package-version',
16+
params: {
17+
org,
18+
name,
19+
version,
20+
},
21+
}
22+
}
23+
24+
// If we have a version param but it isn't a *specific, single version* (e.g. 1.2.3), treat it
25+
// as a semver specifier (e.g. ^1.2.3 or * or 3||4 or >3<=5) and route to the package page with
26+
// the semver query param, which will pre-populate the version selector and show matching versions.
1227
return {
13-
name: 'package-version',
28+
name: 'package',
1429
params: {
1530
org,
1631
name,
17-
// remove spaces to be correctly resolved by router
18-
version: version.replace(/\s+/g, ''),
1932
},
20-
hash,
33+
query: { semver: version },
34+
hash: hash ?? '#versions',
2135
}
2236
}
2337

test/unit/app/utils/router.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { diffRoute, packageRoute } from '../../../../app/utils/router'
3+
4+
describe('packageRoute', () => {
5+
describe('without version', () => {
6+
it('returns package route for unscoped package', () => {
7+
expect(packageRoute('react')).toEqual({
8+
name: 'package',
9+
params: { org: '', name: 'react' },
10+
})
11+
})
12+
13+
it('returns package route for scoped package', () => {
14+
expect(packageRoute('@angular/core')).toEqual({
15+
name: 'package',
16+
params: { org: '@angular', name: 'core' },
17+
})
18+
})
19+
})
20+
21+
describe('with exact version', () => {
22+
it('returns version route for unscoped package', () => {
23+
expect(packageRoute('react', '18.0.0')).toEqual({
24+
name: 'package-version',
25+
params: { org: '', name: 'react', version: '18.0.0' },
26+
})
27+
})
28+
29+
it('returns version route for scoped package', () => {
30+
expect(packageRoute('@angular/core', '18.0.0')).toEqual({
31+
name: 'package-version',
32+
params: { org: '@angular', name: 'core', version: '18.0.0' },
33+
})
34+
})
35+
36+
it('returns version route for prerelease version', () => {
37+
expect(packageRoute('vue', '3.5.0-beta.1')).toEqual({
38+
name: 'package-version',
39+
params: { org: '', name: 'vue', version: '3.5.0-beta.1' },
40+
})
41+
})
42+
})
43+
44+
describe('with version range', () => {
45+
it('returns package route with semver query for caret range', () => {
46+
expect(packageRoute('react', '^18.0.0')).toEqual({
47+
name: 'package',
48+
params: { org: '', name: 'react' },
49+
query: { semver: '^18.0.0' },
50+
hash: '#versions',
51+
})
52+
})
53+
54+
it('returns package route with semver query for tilde range', () => {
55+
expect(packageRoute('react', '~18.2.0')).toEqual({
56+
name: 'package',
57+
params: { org: '', name: 'react' },
58+
query: { semver: '~18.2.0' },
59+
hash: '#versions',
60+
})
61+
})
62+
63+
it('returns package route with semver query for union range', () => {
64+
expect(packageRoute('@angular/core', '^18.0.0 || ^19.0.0 || ^20.0.0')).toEqual({
65+
name: 'package',
66+
params: { org: '@angular', name: 'core' },
67+
query: { semver: '^18.0.0 || ^19.0.0 || ^20.0.0' },
68+
hash: '#versions',
69+
})
70+
})
71+
72+
it('returns package route with semver query for comparator range', () => {
73+
expect(packageRoute('typescript', '>15 <=16.0.2')).toEqual({
74+
name: 'package',
75+
params: { org: '', name: 'typescript' },
76+
query: { semver: '>15 <=16.0.2' },
77+
hash: '#versions',
78+
})
79+
})
80+
81+
it('returns package route with semver query for wildcard', () => {
82+
expect(packageRoute('lodash', '*')).toEqual({
83+
name: 'package',
84+
params: { org: '', name: 'lodash' },
85+
query: { semver: '*' },
86+
hash: '#versions',
87+
})
88+
})
89+
90+
it('returns package route with semver query for dist-tag', () => {
91+
expect(packageRoute('nuxt', 'latest')).toEqual({
92+
name: 'package',
93+
params: { org: '', name: 'nuxt' },
94+
query: { semver: 'latest' },
95+
hash: '#versions',
96+
})
97+
})
98+
})
99+
100+
describe('with null/undefined version', () => {
101+
it('returns package route for null version', () => {
102+
expect(packageRoute('react', null)).toEqual({
103+
name: 'package',
104+
params: { org: '', name: 'react' },
105+
})
106+
})
107+
})
108+
})
109+
110+
describe('diffRoute', () => {
111+
it('returns diff route for unscoped package', () => {
112+
expect(diffRoute('react', '17.0.0', '18.0.0')).toEqual({
113+
name: 'diff',
114+
params: { org: undefined, packageName: 'react', versionRange: '17.0.0...18.0.0' },
115+
})
116+
})
117+
118+
it('returns diff route for scoped package', () => {
119+
expect(diffRoute('@angular/core', '17.0.0', '18.0.0')).toEqual({
120+
name: 'diff',
121+
params: { org: '@angular', packageName: 'core', versionRange: '17.0.0...18.0.0' },
122+
})
123+
})
124+
})

0 commit comments

Comments
 (0)