forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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}