Skip to content

Commit 42862ec

Browse files
fix(ui): resolve name, version and org in usePackageRoute across all
package-related routes
1 parent 368b2b7 commit 42862ec

2 files changed

Lines changed: 169 additions & 10 deletions

File tree

app/composables/usePackageRoute.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,63 @@
11
/**
2-
* Parse package name and optional version from the route URL.
2+
* Parse package name and optional version from the current route URL.
33
*
4-
* Routes use structured params:
5-
* /package/nuxt → org: undefined, name: "nuxt"
6-
* /package/@nuxt/kit → org: "@nuxt", name: "kit"
7-
* /package/nuxt/v/4.2.0 → org: undefined, name: "nuxt", version: "4.2.0"
8-
* /package/@nuxt/kit/v/1.0.0 → org: "@nuxt", name: "kit", version: "1.0.0"
4+
* Works across every package-scoped route, which use different param shapes:
5+
* /package/nuxt → org: undefined, name: "nuxt"
6+
* /package/@nuxt/kit/v/1.0.0 → org: "@nuxt", name: "kit", version: "1.0.0"
7+
* /package-code/@nuxt/kit/v/1.0.0/... → org: "@nuxt", packageName: "kit", version: "1.0.0"
8+
* /package-stats/nuxt/v/4.2.0 → packageName: "nuxt", version: "4.2.0"
9+
* /package-timeline/nuxt/v/4.2.0 → packageName: "nuxt", version: "4.2.0"
10+
* /package-docs/@nuxt/kit/v/1.0.0 → path: ["@nuxt", "kit", "v", "1.0.0"]
11+
*
12+
* Rather than pinning to a single named route, read the live route params and
13+
* normalise the differing param names (`name` vs `packageName`) and the docs
14+
* catch-all `path` into a common `{ org, name, version }` shape.
915
*/
1016
export function usePackageRoute() {
11-
const route = useRoute<'package'>('package')
17+
const route = useRoute()
18+
19+
const parsed = computed<{ org?: string; name?: string; version: string | null }>(() => {
20+
const params = route.params as Record<string, string | string[] | undefined>
21+
22+
// Docs uses a single catch-all `path` param: [org?, name, "v", version?].
23+
// The package prefix is one segment (unscoped) or two (scoped, "@org/name").
24+
// A "v" only marks the version when it directly follows that prefix, so a
25+
// package literally named "v" (e.g. /package-docs/v) isn't mistaken for a
26+
// version delimiter and a later "v" stays part of the package name.
27+
if (Array.isArray(params.path)) {
28+
const segments = params.path.filter(Boolean)
29+
const scoped = segments[0]?.startsWith('@') ?? false
30+
const prefixLength = scoped ? 2 : 1
31+
const org = scoped ? segments[0] : undefined
32+
const name = segments.slice(scoped ? 1 : 0, prefixLength).join('/')
33+
const version =
34+
segments[prefixLength] === 'v' && segments.length > prefixLength + 1
35+
? segments.slice(prefixLength + 1).join('/')
36+
: null
37+
return { org, name, version }
38+
}
39+
40+
const org = typeof params.org === 'string' ? params.org : undefined
41+
// `package`/`changelog` name their param `name`; `code`/`stats`/`timeline`/`diff`
42+
// name it `packageName`.
43+
const name =
44+
(typeof params.name === 'string' ? params.name : undefined) ??
45+
(typeof params.packageName === 'string' ? params.packageName : undefined)
46+
const version = typeof params.version === 'string' ? params.version : null
47+
48+
return { org, name, version }
49+
})
1250

1351
const packageName = computed(() => {
14-
const { org, name } = route.params
52+
const { org, name } = parsed.value
53+
if (!name) return ''
1554
return org ? `${org}/${name}` : name
1655
})
1756

18-
const requestedVersion = computed(() => ('version' in route.params ? route.params.version : null))
57+
const requestedVersion = computed(() => parsed.value.version)
58+
1959
const orgName = computed(() => {
20-
const org = route.params.org
60+
const org = parsed.value.org
2161
return org ? org.replace(/^@/, '') : null
2262
})
2363

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
// `useRoute()` cannot be mocked via `mockNuxtImport` in this runtime, so instead
4+
// we drive the app's real router. That has the upside of exercising the actual
5+
// route definitions: if a route's param names ever change (e.g. `packageName` →
6+
// `name`), these tests break, which is exactly what should happen since
7+
// `usePackageRoute` reads those params.
8+
async function at(url: string) {
9+
await useRouter().push(url)
10+
return usePackageRoute()
11+
}
12+
13+
describe('usePackageRoute', () => {
14+
describe('package / package-version routes (`name` param)', () => {
15+
it('parses an unscoped package with no version', async () => {
16+
const { packageName, requestedVersion, orgName } = await at('/package/nuxt')
17+
expect(packageName.value).toBe('nuxt')
18+
expect(requestedVersion.value).toBeNull()
19+
expect(orgName.value).toBeNull()
20+
})
21+
22+
it('parses a scoped package with a version', async () => {
23+
const { packageName, requestedVersion, orgName } = await at('/package/@nuxt/kit/v/1.0.0')
24+
expect(packageName.value).toBe('@nuxt/kit')
25+
expect(requestedVersion.value).toBe('1.0.0')
26+
expect(orgName.value).toBe('nuxt')
27+
})
28+
})
29+
30+
describe('code / stats / timeline routes (`packageName` param)', () => {
31+
it('parses the code route (scoped, with file path)', async () => {
32+
const { packageName, requestedVersion, orgName } = await at(
33+
'/package-code/@nuxt/kit/v/1.0.0/src/index.ts',
34+
)
35+
expect(packageName.value).toBe('@nuxt/kit')
36+
expect(requestedVersion.value).toBe('1.0.0')
37+
expect(orgName.value).toBe('nuxt')
38+
})
39+
40+
it('parses the stats route (unscoped)', async () => {
41+
const { packageName, requestedVersion, orgName } = await at('/package-stats/nuxt/v/4.2.0')
42+
expect(packageName.value).toBe('nuxt')
43+
expect(requestedVersion.value).toBe('4.2.0')
44+
expect(orgName.value).toBeNull()
45+
})
46+
47+
it('parses the timeline route (scoped)', async () => {
48+
const { packageName, requestedVersion, orgName } = await at(
49+
'/package-timeline/@nuxt/kit/v/1.0.0',
50+
)
51+
expect(packageName.value).toBe('@nuxt/kit')
52+
expect(requestedVersion.value).toBe('1.0.0')
53+
expect(orgName.value).toBe('nuxt')
54+
})
55+
})
56+
57+
describe('changelog routes (`name` param)', () => {
58+
it('parses a scoped package with a version', async () => {
59+
const { packageName, requestedVersion, orgName } = await at(
60+
'/package-changelog/@nuxt/kit/v/1.0.0',
61+
)
62+
expect(packageName.value).toBe('@nuxt/kit')
63+
expect(requestedVersion.value).toBe('1.0.0')
64+
expect(orgName.value).toBe('nuxt')
65+
})
66+
})
67+
68+
describe('docs route (catch-all `path` param)', () => {
69+
it('parses a scoped package with a version', async () => {
70+
const { packageName, requestedVersion, orgName } = await at('/package-docs/@nuxt/kit/v/1.0.0')
71+
expect(packageName.value).toBe('@nuxt/kit')
72+
expect(requestedVersion.value).toBe('1.0.0')
73+
expect(orgName.value).toBe('nuxt')
74+
})
75+
76+
it('parses an unscoped package with a version', async () => {
77+
const { packageName, requestedVersion, orgName } = await at('/package-docs/nuxt/v/4.2.0')
78+
expect(packageName.value).toBe('nuxt')
79+
expect(requestedVersion.value).toBe('4.2.0')
80+
expect(orgName.value).toBeNull()
81+
})
82+
83+
it('parses an unscoped package with no version', async () => {
84+
const { packageName, requestedVersion, orgName } = await at('/package-docs/nuxt')
85+
expect(packageName.value).toBe('nuxt')
86+
expect(requestedVersion.value).toBeNull()
87+
expect(orgName.value).toBeNull()
88+
})
89+
90+
it('parses a scoped package with no version', async () => {
91+
const { packageName, requestedVersion, orgName } = await at('/package-docs/@nuxt/kit')
92+
expect(packageName.value).toBe('@nuxt/kit')
93+
expect(requestedVersion.value).toBeNull()
94+
expect(orgName.value).toBe('nuxt')
95+
})
96+
97+
it('treats a package literally named "v" as the package, not a version marker', async () => {
98+
const { packageName, requestedVersion } = await at('/package-docs/v')
99+
expect(packageName.value).toBe('v')
100+
expect(requestedVersion.value).toBeNull()
101+
})
102+
103+
it('recognises the version marker only when it follows the package name', async () => {
104+
// package "v" at version "1.0.0": the first "v" is the name, the second is the marker
105+
const { packageName, requestedVersion } = await at('/package-docs/v/v/1.0.0')
106+
expect(packageName.value).toBe('v')
107+
expect(requestedVersion.value).toBe('1.0.0')
108+
})
109+
})
110+
111+
describe('diff route (`versionRange` param)', () => {
112+
it('resolves the package/org but does not treat the range as a requested version', async () => {
113+
const { packageName, requestedVersion, orgName } = await at('/diff/@nuxt/kit/v/1.0.0...2.0.0')
114+
expect(packageName.value).toBe('@nuxt/kit')
115+
expect(requestedVersion.value).toBeNull()
116+
expect(orgName.value).toBe('nuxt')
117+
})
118+
})
119+
})

0 commit comments

Comments
 (0)