Skip to content

Commit c045062

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 365bd9f commit c045062

File tree

3 files changed

+144
-4
lines changed

3 files changed

+144
-4
lines changed

app/components/Package/Versions.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ const effectiveCurrentVersion = computed(
110110
() => props.selectedVersion ?? props.distTags.latest ?? undefined,
111111
)
112112
113-
// Semver range filter
114-
const semverFilter = ref('')
113+
// Semver range filter (initialized from ?semver= query param if present)
114+
const semverFilter = ref((typeof route.query.semver === 'string' ? route.query.semver : '') || '')
115115
// Collect all known versions: initial props + dynamically loaded ones
116116
const allKnownVersions = computed(() => {
117117
const versions = new Set(Object.keys(props.versions))

app/utils/router.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
import type { RouteLocationRaw } from 'vue-router'
2+
import { valid as isValidSingleVersion } from 'semver'
23

34
export function packageRoute(packageName: string, version?: string | null): RouteLocationRaw {
45
const [org, name = ''] = packageName.startsWith('@') ? packageName.split('/') : ['', packageName]
56

67
if (version) {
8+
if (isValidSingleVersion(version)) {
9+
return {
10+
name: 'package-version',
11+
params: {
12+
org,
13+
name,
14+
version,
15+
},
16+
}
17+
}
18+
19+
// If we have a version param but it isn't a *specific, single version* (e.g. 1.2.3), treat it
20+
// as a semver specifier (e.g. ^1.2.3 or * or 3||4 or >3<=5) and route to the package page with
21+
// the semver query param, which will pre-populate the version selector and show matching versions.
722
return {
8-
name: 'package-version',
23+
name: 'package',
924
params: {
1025
org,
1126
name,
12-
version,
1327
},
28+
query: { semver: version },
29+
hash: '#versions',
1430
}
1531
}
1632

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)