Skip to content
Merged
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
98 changes: 98 additions & 0 deletions app/composables/useInstallCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { JsrPackageInfo } from '#shared/types/jsr'

/**
* Composable for generating install commands with support for
* multiple package managers, @types packages, and JSR.
*/
export function useInstallCommand(
packageName: MaybeRefOrGetter<string | null>,
requestedVersion: MaybeRefOrGetter<string | null>,
jsrInfo: MaybeRefOrGetter<JsrPackageInfo | null>,
typesPackageName: MaybeRefOrGetter<string | null>,
) {
const selectedPM = useSelectedPackageManager()
const { settings } = useSettings()

// Check if we should show @types in install command
const showTypesInInstall = computed(() => {
return settings.value.includeTypesInInstall && !!toValue(typesPackageName)
})

const installCommandParts = computed(() => {
const name = toValue(packageName)
if (!name) return []
return getInstallCommandParts({
packageName: name,
packageManager: selectedPM.value,
version: toValue(requestedVersion),
jsrInfo: toValue(jsrInfo),
})
})

const installCommand = computed(() => {
const name = toValue(packageName)
if (!name) return ''
return getInstallCommand({
packageName: name,
packageManager: selectedPM.value,
version: toValue(requestedVersion),
jsrInfo: toValue(jsrInfo),
})
})

// Get the dev dependency flag for the selected package manager
const devFlag = computed(() => {
// bun uses lowercase -d, all others use -D
return selectedPM.value === 'bun' ? '-d' : '-D'
})

// @types install command parts (for display)
const typesInstallCommandParts = computed(() => {
const types = toValue(typesPackageName)
if (!types) return []
const pm = packageManagers.find(p => p.id === selectedPM.value)
if (!pm) return []

const pkgSpec = selectedPM.value === 'deno' ? `npm:${types}` : types

return [pm.label, pm.action, devFlag.value, pkgSpec]
})

// Full install command including @types (for copying)
const fullInstallCommand = computed(() => {
if (!installCommand.value) return ''
const types = toValue(typesPackageName)
if (!showTypesInInstall.value || !types) {
return installCommand.value
}

const pm = packageManagers.find(p => p.id === selectedPM.value)
if (!pm) return installCommand.value

const pkgSpec = selectedPM.value === 'deno' ? `npm:${types}` : types

// Use semicolon to separate commands
return `${installCommand.value}; ${pm.label} ${pm.action} ${devFlag.value} ${pkgSpec}`
})

// Copy state
const copied = ref(false)

async function copyInstallCommand() {
if (!fullInstallCommand.value) return
await navigator.clipboard.writeText(fullInstallCommand.value)
copied.value = true
setTimeout(() => (copied.value = false), 2000)
}

return {
selectedPM,
installCommandParts,
installCommand,
typesInstallCommandParts,
fullInstallCommand,
showTypesInInstall,
copied,
copyInstallCommand,
}
}
50 changes: 22 additions & 28 deletions app/composables/useNpmRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,43 +114,37 @@ export function usePackage(
) {
const cachedFetch = useCachedFetch()

const asyncData = useLazyAsyncData(
return useLazyAsyncData(
() => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`,
async () => {
const encodedName = encodePackageName(toValue(name))
const pkg = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`)
return transformPackument(pkg, toValue(requestedVersion))
const r = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`)
const reqVer = toValue(requestedVersion)
const pkg = transformPackument(r, reqVer)
const resolvedVersion = getResolvedVersion(pkg, reqVer)
return { ...pkg, resolvedVersion }
},
)
}

// Resolve requestedVersion to an exact version
// Handles: exact versions, dist-tags (latest, next), and semver ranges (^4.2, >=1.0.0)
const resolvedVersion = computed(() => {
const pkg = asyncData.data.value
const reqVer = toValue(requestedVersion)
if (!pkg || !reqVer) return null
function getResolvedVersion(pkg: SlimPackument, reqVer?: string | null): string | null {
if (!pkg || !reqVer) return null

// 1. Check if it's already an exact version in pkg.versions
if (isExactVersion(reqVer) && pkg.versions[reqVer]) {
return reqVer
}

// 2. Check if it's a dist-tag (latest, next, beta, etc.)
const tagVersion = pkg['dist-tags']?.[reqVer]
if (tagVersion) {
return tagVersion
}

// 3. Try to resolve as a semver range
const versions = Object.keys(pkg.versions)
const resolved = maxSatisfying(versions, reqVer)
return resolved
})
// 1. Check if it's already an exact version in pkg.versions
if (isExactVersion(reqVer) && pkg.versions[reqVer]) {
return reqVer
}

return {
...asyncData,
resolvedVersion,
// 2. Check if it's a dist-tag (latest, next, beta, etc.)
const tagVersion = pkg['dist-tags']?.[reqVer]
if (tagVersion) {
return tagVersion
}

// 3. Try to resolve as a semver range
const versions = Object.keys(pkg.versions)
const resolved = maxSatisfying(versions, reqVer)
return resolved
}

export function usePackageDownloads(
Expand Down
60 changes: 60 additions & 0 deletions app/composables/usePackageRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Parse package name and optional version from the route URL.
*
* Supported patterns:
* /nuxt → packageName: "nuxt", requestedVersion: null
* /nuxt/v/4.2.0 → packageName: "nuxt", requestedVersion: "4.2.0"
* /@nuxt/kit → packageName: "@nuxt/kit", requestedVersion: null
* /@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", requestedVersion: "1.0.0"
* /axios@1.13.3 → packageName: "axios", requestedVersion: "1.13.3"
* /@nuxt/kit@1.0.0 → packageName: "@nuxt/kit", requestedVersion: "1.0.0"
*/
export function usePackageRoute() {
const route = useRoute('package')

const parsedRoute = computed(() => {
const segments = route.params.package || []

// Find the /v/ separator for version
const vIndex = segments.indexOf('v')
if (vIndex !== -1 && vIndex < segments.length - 1) {
return {
packageName: segments.slice(0, vIndex).join('/'),
requestedVersion: segments.slice(vIndex + 1).join('/'),
}
}

// Parse @ versioned package
const fullPath = segments.join('/')
const versionMatch = fullPath.match(/^(@[^/]+\/[^/]+|[^/]+)@([^/]+)$/)
if (versionMatch) {
const [, packageName, requestedVersion] = versionMatch as [string, string, string]
return {
packageName,
requestedVersion,
}
}

return {
packageName: fullPath,
requestedVersion: null as string | null,
}
})

const packageName = computed(() => parsedRoute.value.packageName)
const requestedVersion = computed(() => parsedRoute.value.requestedVersion)

// Extract org name from scoped package (e.g., "@nuxt/kit" -> "nuxt")
const orgName = computed(() => {
const name = packageName.value
if (!name.startsWith('@')) return null
const match = name.match(/^@([^/]+)\//)
return match ? match[1] : null
})

return {
packageName,
requestedVersion,
orgName,
}
}
Loading