Skip to content

Commit 35afdfe

Browse files
committed
refactor: pull out composables from package page + add tests
1 parent 6f6f695 commit 35afdfe

5 files changed

Lines changed: 731 additions & 136 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { JsrPackageInfo } from '#shared/types/jsr'
2+
3+
/**
4+
* Composable for generating install commands with support for
5+
* multiple package managers, @types packages, and JSR.
6+
*/
7+
export function useInstallCommand(
8+
packageName: MaybeRefOrGetter<string | null>,
9+
requestedVersion: MaybeRefOrGetter<string | null>,
10+
jsrInfo: MaybeRefOrGetter<JsrPackageInfo | null>,
11+
typesPackageName: MaybeRefOrGetter<string | null>,
12+
) {
13+
const selectedPM = useSelectedPackageManager()
14+
const { settings } = useSettings()
15+
16+
// Check if we should show @types in install command
17+
const showTypesInInstall = computed(() => {
18+
return settings.value.includeTypesInInstall && toValue(typesPackageName)
19+
})
20+
21+
const installCommandParts = computed(() => {
22+
const name = toValue(packageName)
23+
if (!name) return []
24+
return getInstallCommandParts({
25+
packageName: name,
26+
packageManager: selectedPM.value,
27+
version: toValue(requestedVersion),
28+
jsrInfo: toValue(jsrInfo),
29+
})
30+
})
31+
32+
const installCommand = computed(() => {
33+
const name = toValue(packageName)
34+
if (!name) return ''
35+
return getInstallCommand({
36+
packageName: name,
37+
packageManager: selectedPM.value,
38+
version: toValue(requestedVersion),
39+
jsrInfo: toValue(jsrInfo),
40+
})
41+
})
42+
43+
// Get the dev dependency flag for the selected package manager
44+
const devFlag = computed(() => {
45+
// bun uses lowercase -d, all others use -D
46+
return selectedPM.value === 'bun' ? '-d' : '-D'
47+
})
48+
49+
// @types install command parts (for display)
50+
const typesInstallCommandParts = computed(() => {
51+
const types = toValue(typesPackageName)
52+
if (!types) return []
53+
const pm = packageManagers.find(p => p.id === selectedPM.value)
54+
if (!pm) return []
55+
56+
const pkgSpec = selectedPM.value === 'deno' ? `npm:${types}` : types
57+
58+
return [pm.label, pm.action, devFlag.value, pkgSpec]
59+
})
60+
61+
// Full install command including @types (for copying)
62+
const fullInstallCommand = computed(() => {
63+
if (!installCommand.value) return ''
64+
const types = toValue(typesPackageName)
65+
if (!showTypesInInstall.value || !types) {
66+
return installCommand.value
67+
}
68+
69+
const pm = packageManagers.find(p => p.id === selectedPM.value)
70+
if (!pm) return installCommand.value
71+
72+
const pkgSpec = selectedPM.value === 'deno' ? `npm:${types}` : types
73+
74+
// Use semicolon to separate commands
75+
return `${installCommand.value}; ${pm.label} ${pm.action} ${devFlag.value} ${pkgSpec}`
76+
})
77+
78+
// Copy state
79+
const copied = ref(false)
80+
81+
async function copyInstallCommand() {
82+
if (!fullInstallCommand.value) return
83+
await navigator.clipboard.writeText(fullInstallCommand.value)
84+
copied.value = true
85+
setTimeout(() => (copied.value = false), 2000)
86+
}
87+
88+
return {
89+
selectedPM,
90+
installCommandParts,
91+
installCommand,
92+
typesInstallCommandParts,
93+
fullInstallCommand,
94+
showTypesInInstall,
95+
copied,
96+
copyInstallCommand,
97+
}
98+
}

app/composables/usePackageRoute.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Parse package name and optional version from the route URL.
3+
*
4+
* Supported patterns:
5+
* /nuxt → packageName: "nuxt", requestedVersion: null
6+
* /nuxt/v/4.2.0 → packageName: "nuxt", requestedVersion: "4.2.0"
7+
* /@nuxt/kit → packageName: "@nuxt/kit", requestedVersion: null
8+
* /@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", requestedVersion: "1.0.0"
9+
* /axios@1.13.3 → packageName: "axios", requestedVersion: "1.13.3"
10+
* /@nuxt/kit@1.0.0 → packageName: "@nuxt/kit", requestedVersion: "1.0.0"
11+
*/
12+
export function usePackageRoute() {
13+
const route = useRoute('package')
14+
15+
const parsedRoute = computed(() => {
16+
const segments = route.params.package || []
17+
18+
// Find the /v/ separator for version
19+
const vIndex = segments.indexOf('v')
20+
if (vIndex !== -1 && vIndex < segments.length - 1) {
21+
return {
22+
packageName: segments.slice(0, vIndex).join('/'),
23+
requestedVersion: segments.slice(vIndex + 1).join('/'),
24+
}
25+
}
26+
27+
// Parse @ versioned package
28+
const fullPath = segments.join('/')
29+
const versionMatch = fullPath.match(/^(@[^/]+\/[^/]+|[^/]+)@([^/]+)$/)
30+
if (versionMatch) {
31+
const [, packageName, requestedVersion] = versionMatch as [string, string, string]
32+
return {
33+
packageName,
34+
requestedVersion,
35+
}
36+
}
37+
38+
return {
39+
packageName: fullPath,
40+
requestedVersion: null as string | null,
41+
}
42+
})
43+
44+
const packageName = computed(() => parsedRoute.value.packageName)
45+
const requestedVersion = computed(() => parsedRoute.value.requestedVersion)
46+
47+
// Extract org name from scoped package (e.g., "@nuxt/kit" -> "nuxt")
48+
const orgName = computed(() => {
49+
const name = packageName.value
50+
if (!name.startsWith('@')) return null
51+
const match = name.match(/^@([^/]+)\//)
52+
return match ? match[1] : null
53+
})
54+
55+
return {
56+
packageName,
57+
requestedVersion,
58+
orgName,
59+
}
60+
}

app/pages/[...package].vue

Lines changed: 22 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -11,62 +11,14 @@ definePageMeta({
1111
alias: ['/package/:package(.*)*'],
1212
})
1313
14-
const route = useRoute('package')
15-
1614
const router = useRouter()
1715
18-
// Parse package name and optional version from URL
19-
// Patterns:
20-
// /nuxt → packageName: "nuxt", requestedVersion: null
21-
// /nuxt/v/4.2.0 → packageName: "nuxt", requestedVersion: "4.2.0"
22-
// /@nuxt/kit → packageName: "@nuxt/kit", requestedVersion: null
23-
// /@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", requestedVersion: "1.0.0"
24-
// /axios@1.13.3 → packageName: "axios", requestedVersion: "1.13.3"
25-
// /@nuxt/kit@1.0.0 → packageName: "@nuxt/kit", requestedVersion: "1.0.0"
26-
const parsedRoute = computed(() => {
27-
const segments = route.params.package || []
28-
29-
// Find the /v/ separator for version
30-
const vIndex = segments.indexOf('v')
31-
if (vIndex !== -1 && vIndex < segments.length - 1) {
32-
return {
33-
packageName: segments.slice(0, vIndex).join('/'),
34-
requestedVersion: segments.slice(vIndex + 1).join('/'),
35-
}
36-
}
37-
38-
// Parse @ versioned package
39-
const fullPath = segments.join('/')
40-
const versionMatch = fullPath.match(/^(@[^/]+\/[^/]+|[^/]+)@([^/]+)$/)
41-
if (versionMatch) {
42-
const [, packageName, requestedVersion] = versionMatch as [string, string, string]
43-
return {
44-
packageName,
45-
requestedVersion,
46-
}
47-
}
48-
49-
return {
50-
packageName: fullPath,
51-
requestedVersion: null as string | null,
52-
}
53-
})
54-
55-
const packageName = computed(() => parsedRoute.value.packageName)
56-
const requestedVersion = computed(() => parsedRoute.value.requestedVersion)
16+
const { packageName, requestedVersion, orgName } = usePackageRoute()
5717
5818
if (import.meta.server) {
5919
assertValidPackageName(packageName.value)
6020
}
6121
62-
// Extract org name from scoped package (e.g., "@nuxt/kit" -> "nuxt")
63-
const orgName = computed(() => {
64-
const name = packageName.value
65-
if (!name.startsWith('@')) return null
66-
const match = name.match(/^@([^/]+)\//)
67-
return match ? match[1] : null
68-
})
69-
7022
const { data: downloads } = usePackageDownloads(packageName, 'last-week')
7123
const { data: weeklyDownloads } = usePackageWeeklyDownloadEvolution(packageName, { weeks: 52 })
7224
@@ -112,17 +64,7 @@ const {
11264
)
11365
onMounted(() => fetchInstallSize())
11466
115-
const sizeTooltip = computed(() => {
116-
const chunks = [
117-
displayVersion.value &&
118-
displayVersion.value.dist.unpackedSize &&
119-
`${formatBytes(displayVersion.value.dist.unpackedSize)} unpacked size (this package)`,
120-
installSize.value &&
121-
installSize.value.dependencyCount &&
122-
`${formatBytes(installSize.value.totalSize)} total unpacked size (including all ${installSize.value.dependencyCount} dependencies for linux-x64)`,
123-
]
124-
return chunks.filter(Boolean).join('\n')
125-
})
67+
const { data: packageAnalysis } = usePackageAnalysis(packageName, requestedVersion)
12668
12769
const { data: pkg, status, error } = await usePackage(packageName, requestedVersion)
12870
const resolvedVersion = computed(() => pkg.value?.resolvedVersion ?? null)
@@ -164,6 +106,18 @@ const deprecationNotice = computed(() => {
164106
return { type: 'version' as const, message: displayVersion.value.deprecated }
165107
})
166108
109+
const sizeTooltip = computed(() => {
110+
const chunks = [
111+
displayVersion.value &&
112+
displayVersion.value.dist.unpackedSize &&
113+
`${formatBytes(displayVersion.value.dist.unpackedSize)} unpacked size (this package)`,
114+
installSize.value &&
115+
installSize.value.dependencyCount &&
116+
`${formatBytes(installSize.value.totalSize)} total unpacked size (including all ${installSize.value.dependencyCount} dependencies for linux-x64)`,
117+
]
118+
return chunks.filter(Boolean).join('\n')
119+
})
120+
167121
const hasDependencies = computed(() => {
168122
if (!displayVersion.value) return false
169123
const deps = displayVersion.value.dependencies
@@ -252,12 +206,6 @@ function hasProvenance(version: PackumentVersion | null): boolean {
252206
return !!dist.attestations
253207
}
254208
255-
const selectedPM = useSelectedPackageManager()
256-
const { settings } = useSettings()
257-
258-
// Fetch package analysis for @types info
259-
const { data: packageAnalysis } = usePackageAnalysis(packageName, requestedVersion)
260-
261209
// Get @types package name if available (non-deprecated)
262210
const typesPackageName = computed(() => {
263211
if (!packageAnalysis.value) return null
@@ -266,76 +214,14 @@ const typesPackageName = computed(() => {
266214
return packageAnalysis.value.types.packageName
267215
})
268216
269-
// Check if we should show @types in install command
270-
const showTypesInInstall = computed(() => {
271-
return settings.value.includeTypesInInstall && typesPackageName.value
272-
})
273-
274-
const installCommandParts = computed(() => {
275-
if (!pkg.value) return []
276-
return getInstallCommandParts({
277-
packageName: pkg.value.name,
278-
packageManager: selectedPM.value,
279-
version: requestedVersion.value,
280-
jsrInfo: jsrInfo.value,
281-
})
282-
})
283-
284-
const installCommand = computed(() => {
285-
if (!pkg.value) return ''
286-
return getInstallCommand({
287-
packageName: pkg.value.name,
288-
packageManager: selectedPM.value,
289-
version: requestedVersion.value,
290-
jsrInfo: jsrInfo.value,
291-
})
292-
})
293-
294-
// Get the dev dependency flag for the selected package manager
295-
function getDevFlag(pmId: string): string {
296-
// bun uses lowercase -d, all others use -D
297-
return pmId === 'bun' ? '-d' : '-D'
298-
}
299-
300-
// @types install command parts (for display)
301-
const typesInstallCommandParts = computed(() => {
302-
if (!typesPackageName.value) return []
303-
const pm = packageManagers.find(p => p.id === selectedPM.value)
304-
if (!pm) return []
305-
306-
const devFlag = getDevFlag(selectedPM.value)
307-
const pkgSpec =
308-
selectedPM.value === 'deno' ? `npm:${typesPackageName.value}` : typesPackageName.value
309-
310-
return [pm.label, pm.action, devFlag, pkgSpec]
311-
})
312-
313-
// Full install command including @types (for copying)
314-
const fullInstallCommand = computed(() => {
315-
if (!installCommand.value) return ''
316-
if (!showTypesInInstall.value || !typesPackageName.value) {
317-
return installCommand.value
318-
}
319-
320-
const pm = packageManagers.find(p => p.id === selectedPM.value)
321-
if (!pm) return installCommand.value
322-
323-
const devFlag = getDevFlag(selectedPM.value)
324-
const pkgSpec =
325-
selectedPM.value === 'deno' ? `npm:${typesPackageName.value}` : typesPackageName.value
326-
327-
// Use semicolon to separate commands
328-
return `${installCommand.value}; ${pm.label} ${pm.action} ${devFlag} ${pkgSpec}`
329-
})
330-
331-
// Copy install command
332-
const copied = ref(false)
333-
async function copyInstallCommand() {
334-
if (!fullInstallCommand.value) return
335-
await navigator.clipboard.writeText(fullInstallCommand.value)
336-
copied.value = true
337-
setTimeout(() => (copied.value = false), 2000)
338-
}
217+
const {
218+
selectedPM,
219+
installCommandParts,
220+
typesInstallCommandParts,
221+
showTypesInInstall,
222+
copied,
223+
copyInstallCommand,
224+
} = useInstallCommand(packageName, requestedVersion, jsrInfo, typesPackageName)
339225
340226
// Expandable description
341227
const descriptionExpanded = ref(false)

0 commit comments

Comments
 (0)