Skip to content

Commit 465ce2c

Browse files
authored
feat: add radicle + forgejo, fix tangled meta, ensure meta is fetched on ssr (#154)
1 parent f09077a commit 465ce2c

9 files changed

Lines changed: 1044 additions & 184 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/useNpmRegistry.ts

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -114,43 +114,37 @@ export function usePackage(
114114
) {
115115
const cachedFetch = useCachedFetch()
116116

117-
const asyncData = useLazyAsyncData(
117+
return useLazyAsyncData(
118118
() => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`,
119119
async () => {
120120
const encodedName = encodePackageName(toValue(name))
121-
const pkg = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`)
122-
return transformPackument(pkg, toValue(requestedVersion))
121+
const r = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`)
122+
const reqVer = toValue(requestedVersion)
123+
const pkg = transformPackument(r, reqVer)
124+
const resolvedVersion = getResolvedVersion(pkg, reqVer)
125+
return { ...pkg, resolvedVersion }
123126
},
124127
)
128+
}
125129

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

133-
// 1. Check if it's already an exact version in pkg.versions
134-
if (isExactVersion(reqVer) && pkg.versions[reqVer]) {
135-
return reqVer
136-
}
137-
138-
// 2. Check if it's a dist-tag (latest, next, beta, etc.)
139-
const tagVersion = pkg['dist-tags']?.[reqVer]
140-
if (tagVersion) {
141-
return tagVersion
142-
}
143-
144-
// 3. Try to resolve as a semver range
145-
const versions = Object.keys(pkg.versions)
146-
const resolved = maxSatisfying(versions, reqVer)
147-
return resolved
148-
})
133+
// 1. Check if it's already an exact version in pkg.versions
134+
if (isExactVersion(reqVer) && pkg.versions[reqVer]) {
135+
return reqVer
136+
}
149137

150-
return {
151-
...asyncData,
152-
resolvedVersion,
138+
// 2. Check if it's a dist-tag (latest, next, beta, etc.)
139+
const tagVersion = pkg['dist-tags']?.[reqVer]
140+
if (tagVersion) {
141+
return tagVersion
153142
}
143+
144+
// 3. Try to resolve as a semver range
145+
const versions = Object.keys(pkg.versions)
146+
const resolved = maxSatisfying(versions, reqVer)
147+
return resolved
154148
}
155149

156150
export function usePackageDownloads(

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+
}

0 commit comments

Comments
 (0)