[READ-ONLY] a fast, modern browser for the npm registry
at main 167 lines 5.5 kB view raw
1import type { 2 Packument, 3 SlimPackument, 4 SlimVersion, 5 SlimPackumentVersion, 6 PackumentVersion, 7 PublishTrustLevel, 8} from '#shared/types' 9import { extractInstallScriptsInfo } from '~/utils/install-scripts' 10 11/** Number of recent versions to include in initial payload */ 12const RECENT_VERSIONS_COUNT = 5 13 14function hasAttestations(version: PackumentVersion): boolean { 15 return Boolean(version.dist.attestations) 16} 17 18function hasTrustedPublisher(version: PackumentVersion): boolean { 19 return Boolean(version._npmUser?.trustedPublisher) 20} 21 22function getTrustLevel(version: PackumentVersion): PublishTrustLevel { 23 // trusted publishing automatically generates provenance attestations 24 if (hasTrustedPublisher(version)) return 'trustedPublisher' 25 if (hasAttestations(version)) return 'provenance' 26 return 'none' 27} 28 29/** 30 * Transform a full Packument into a slimmed version for client-side use. 31 * Reduces payload size by: 32 * - Removing readme (fetched separately) 33 * - Including only: 5 most recent versions + one version per dist-tag + requested version 34 * - Stripping unnecessary fields from version objects 35 */ 36export function transformPackument( 37 pkg: Packument, 38 requestedVersion?: string | null, 39): SlimPackument { 40 // Get versions pointed to by dist-tags 41 const distTagVersions = new Set(Object.values(pkg['dist-tags'] ?? {})) 42 43 // Get 5 most recent versions by publish time 44 const recentVersions = Object.keys(pkg.versions) 45 .filter(v => pkg.time[v]) 46 .sort((a, b) => { 47 const timeA = pkg.time[a] 48 const timeB = pkg.time[b] 49 if (!timeA || !timeB) return 0 50 return new Date(timeB).getTime() - new Date(timeA).getTime() 51 }) 52 .slice(0, RECENT_VERSIONS_COUNT) 53 54 // Combine: recent versions + dist-tag versions + requested version (deduplicated) 55 const includedVersions = new Set([...recentVersions, ...distTagVersions]) 56 57 // Add the requested version if it exists in the package 58 if (requestedVersion && pkg.versions[requestedVersion]) { 59 includedVersions.add(requestedVersion) 60 } 61 62 // Build security metadata for all versions, but only include in payload 63 // when the package has mixed trust levels (i.e. a downgrade could exist) 64 const securityVersionEntries = Object.entries(pkg.versions).map(([version, metadata]) => { 65 const trustLevel = getTrustLevel(metadata) 66 return { 67 version, 68 time: pkg.time[version], 69 hasProvenance: trustLevel !== 'none', 70 trustLevel, 71 deprecated: metadata.deprecated, 72 } 73 }) 74 75 const trustLevels = new Set(securityVersionEntries.map(v => v.trustLevel)) 76 const hasMixedTrust = trustLevels.size > 1 77 const securityVersions = hasMixedTrust ? securityVersionEntries : undefined 78 79 // Build filtered versions object with install scripts info per version 80 const filteredVersions: Record<string, SlimVersion> = {} 81 let versionData: SlimPackumentVersion | null = null 82 for (const v of includedVersions) { 83 const version = pkg.versions[v] 84 if (version) { 85 if (version.version === requestedVersion) { 86 // Strip readme from each version, extract install scripts info 87 const { readme: _readme, scripts, ...slimVersion } = version 88 89 // Extract install scripts info (which scripts exist + npx deps) 90 const installScripts = scripts ? extractInstallScriptsInfo(scripts) : null 91 versionData = { 92 ...slimVersion, 93 installScripts: installScripts ?? undefined, 94 } 95 } 96 const trustLevel = getTrustLevel(version) 97 const hasProvenance = trustLevel !== 'none' 98 99 filteredVersions[v] = { 100 hasProvenance, 101 trustLevel, 102 version: version.version, 103 deprecated: version.deprecated, 104 tags: version.tags as string[], 105 } 106 } 107 } 108 109 // Build filtered time object (only for included versions + metadata) 110 const filteredTime: Record<string, string> = {} 111 if (pkg.time.modified) filteredTime.modified = pkg.time.modified 112 if (pkg.time.created) filteredTime.created = pkg.time.created 113 for (const v of includedVersions) { 114 if (pkg.time[v]) filteredTime[v] = pkg.time[v] 115 } 116 117 // Normalize license field 118 let license = pkg.license 119 if (license && typeof license === 'object' && 'type' in license) { 120 license = license.type 121 } 122 123 return { 124 '_id': pkg._id, 125 '_rev': pkg._rev, 126 'name': pkg.name, 127 'description': pkg.description, 128 'dist-tags': pkg['dist-tags'], 129 'time': filteredTime, 130 'maintainers': pkg.maintainers, 131 'author': pkg.author, 132 'license': license, 133 'homepage': pkg.homepage, 134 'keywords': pkg.keywords, 135 'repository': pkg.repository, 136 'bugs': pkg.bugs, 137 'requestedVersion': versionData, 138 'versions': filteredVersions, 139 'securityVersions': securityVersions, 140 } 141} 142 143export function usePackage( 144 name: MaybeRefOrGetter<string>, 145 requestedVersion?: MaybeRefOrGetter<string | null>, 146) { 147 const asyncData = useLazyAsyncData( 148 () => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`, 149 async ({ $npmRegistry }, { signal }) => { 150 const encodedName = encodePackageName(toValue(name)) 151 const { data: r, isStale } = await $npmRegistry<Packument>(`/${encodedName}`, { 152 signal, 153 }) 154 const reqVer = toValue(requestedVersion) 155 const pkg = transformPackument(r, reqVer) 156 return { ...pkg, isStale } 157 }, 158 ) 159 160 if (import.meta.client && asyncData.data.value?.isStale) { 161 onMounted(() => { 162 asyncData.refresh() 163 }) 164 } 165 166 return asyncData 167}