[READ-ONLY] a fast, modern browser for the npm registry
at main 141 lines 4.3 kB view raw
1import type { Packument, NpmSearchResponse } from '#shared/types' 2import { encodePackageName, fetchLatestVersion } from '#shared/utils/npm' 3import { maxSatisfying, prerelease } from 'semver' 4import { CACHE_MAX_AGE_FIVE_MINUTES } from '#shared/utils/constants' 5 6const NPM_REGISTRY = 'https://registry.npmjs.org' 7 8export const fetchNpmPackage = defineCachedFunction( 9 async (name: string): Promise<Packument> => { 10 const encodedName = encodePackageName(name) 11 return await $fetch<Packument>(`${NPM_REGISTRY}/${encodedName}`) 12 }, 13 { 14 maxAge: CACHE_MAX_AGE_FIVE_MINUTES, 15 swr: true, 16 name: 'npm-package', 17 getKey: (name: string) => name, 18 }, 19) 20 21/** 22 * Get the latest version of a package using fast-npm-meta API. 23 * Falls back to full packument if fast-npm-meta fails. 24 * 25 * @param name Package name 26 * @returns Latest version string or null if not found 27 */ 28export async function fetchLatestVersionWithFallback(name: string): Promise<string | null> { 29 const version = await fetchLatestVersion(name) 30 if (version) return version 31 32 // Fallback to full packument (also cached) 33 try { 34 const packument = await fetchNpmPackage(name) 35 return packument['dist-tags']?.latest ?? null 36 } catch { 37 return null 38 } 39} 40 41/** 42 * Check if a version constraint explicitly includes a prerelease tag. 43 * e.g., "^1.0.0-alpha" or ">=2.0.0-beta.1" include prereleases 44 */ 45function constraintIncludesPrerelease(constraint: string): boolean { 46 // Look for prerelease identifiers in the constraint 47 return ( 48 /-(?:alpha|beta|rc|next|canary|dev|preview|pre|experimental)/i.test(constraint) || 49 /-\d/.test(constraint) 50 ) // e.g., -0, -1 51} 52 53/** 54 * Resolve a semver version constraint to the best matching version. 55 * Returns the highest version that satisfies the constraint, or null if none match. 56 * 57 * By default, excludes prerelease versions unless the constraint explicitly 58 * includes a prerelease tag (e.g., "^1.0.0-beta"). 59 */ 60export async function resolveVersionConstraint( 61 packageName: string, 62 constraint: string, 63): Promise<string | null> { 64 try { 65 const packument = await fetchNpmPackage(packageName) 66 let versions = Object.keys(packument.versions) 67 68 // Filter out prerelease versions unless constraint explicitly includes one 69 if (!constraintIncludesPrerelease(constraint)) { 70 versions = versions.filter(v => !prerelease(v)) 71 } 72 73 return maxSatisfying(versions, constraint) 74 } catch { 75 return null 76 } 77} 78 79/** 80 * Resolve multiple dependency constraints to their best matching versions. 81 * Returns a map of package name to resolved version. 82 */ 83export async function resolveDependencyVersions( 84 dependencies: Record<string, string>, 85): Promise<Record<string, string>> { 86 const entries = Object.entries(dependencies) 87 const results = await Promise.all( 88 entries.map(async ([name, constraint]) => { 89 const resolved = await resolveVersionConstraint(name, constraint) 90 return [name, resolved] as const 91 }), 92 ) 93 94 const resolved: Record<string, string> = {} 95 for (const [name, version] of results) { 96 if (version) { 97 resolved[name] = version 98 } 99 } 100 return resolved 101} 102 103/** 104 * Find a user's email address from its username 105 * by exploring metadata in its public packages 106 */ 107export const fetchUserEmail = defineCachedFunction( 108 async (username: string): Promise<string | null> => { 109 const handle = username.trim() 110 if (!handle) return null 111 112 // Fetch packages with the user's handle as a maintainer 113 const params = new URLSearchParams({ 114 text: `maintainer:${handle}`, 115 size: '20', 116 }) 117 const response = await $fetch<NpmSearchResponse>(`${NPM_REGISTRY}/-/v1/search?${params}`) 118 const lowerHandle = handle.toLowerCase() 119 120 // Search for the user's email in packages metadata 121 for (const result of response.objects) { 122 const maintainers = result.package.maintainers ?? [] 123 const match = maintainers.find( 124 person => 125 person.username?.toLowerCase() === lowerHandle || 126 person.name?.toLowerCase() === lowerHandle, 127 ) 128 if (match?.email) { 129 return match.email 130 } 131 } 132 133 return null 134 }, 135 { 136 maxAge: CACHE_MAX_AGE_ONE_DAY, 137 swr: true, 138 name: 'npm-user-email', 139 getKey: (username: string) => `npm-user-email:${username.trim().toLowerCase()}`, 140 }, 141)