import is from '@sindresorhus/is' ; import pAll from 'p-all' ; import semver from 'semver' ; import { XmlDocument } from 'xmldoc' ; import { logger } from '../../logger' ; import { ExternalHostError } from '../../types/errors/external-host-error' ; import * as packageCache from '../../util/cache/package' ; import { Http } from '../../util/http' ; import { HttpError } from '../../util/http/types' ; import { regEx } from '../../util/regex' ; import { ensureTrailingSlash } from '../../util/url' ; import type { Release , ReleaseResult } from '../types' ; import { id , removeBuildMeta } from './common' ; import type { CatalogEntry , CatalogPage , PackageRegistration , ServicesIndexRaw , } from './types' ; const http = new Http ( id ) ; const defaultNugetFeed = 'https://api.nuget.org/v3/index.json' ; const cacheNamespace = 'datasource-nuget' ; export function getDefaultFeed ( ) { return defaultNugetFeed ; } export async function getResourceUrl ( url , resourceType = 'RegistrationsBaseUrl' ) { const resultCacheKey = `${ url }:${ resourceType }` ; const cachedResult = await packageCache . get < string > ( cacheNamespace , resultCacheKey ) ; if ( cachedResult ) { return cachedResult ; } try { const responseCacheKey = url ; let servicesIndexRaw = await packageCache . get < ServicesIndexRaw > ( cacheNamespace , responseCacheKey ) ; if ( ! servicesIndexRaw ) { servicesIndexRaw = ( await http . getJson < ServicesIndexRaw > ( url ) ) . body ; await packageCache . set ( cacheNamespace , responseCacheKey , servicesIndexRaw , 3 * 24 * 60 ) ; } const services = servicesIndexRaw . resources . map ( ( { '@id' : serviceId , '@type' : t } ) => ( { serviceId , type : t ?. split ( '/' ) ?. shift ( ) , version : t ?. split ( '/' ) ?. pop ( ) , } ) ) . filter ( ( { type , version } ) => type === resourceType && semver . valid ( version ) ) . sort ( ( x , y ) => semver . compare ( x . version , y . version ) ) ; const { serviceId , version } = services . pop ( ) ; if ( resourceType === 'RegistrationsBaseUrl' && ! version ?. startsWith ( '3.0.0-' ) && ! semver . satisfies ( version , '^3.0.0' ) ) { logger . warn ( { url , version } , `Nuget: Unknown version returned. Only v3 is supported` ) ; } await packageCache . set ( cacheNamespace , resultCacheKey , serviceId , 60 ) ; return serviceId ; } catch ( err ) { if ( err instanceof ExternalHostError ) { throw err ; } logger . debug ( { err , url } , `nuget registry failure: can't get ${ resourceType }` ) ; return null ; } } async function getCatalogEntry ( catalogPage ) { let items = catalogPage . items ; if ( ! items ) { const url = catalogPage [ '@id' ] ; const catalogPageFull = await http . getJson < CatalogPage > ( url ) ; items = catalogPageFull . body . items ; } return items . map ( ( { catalogEntry } ) => catalogEntry ) ; } export async function getReleases ( registryUrl , feedUrl , pkgName ) { const baseUrl = feedUrl . replace ( regEx ( /\/*$/ ) , '' ) ; const url = `${ baseUrl }/${ pkgName . toLowerCase ( ) }/index.json` ; const packageRegistration = await http . getJson < PackageRegistration > ( url ) ; const catalogPages = packageRegistration . body . items || [ ] ; const catalogPagesQueue = catalogPages . map ( ( page ) => ( ) : Promise < CatalogEntry [ ] > => getCatalogEntry ( page ) ) ; const catalogEntries = ( await pAll ( catalogPagesQueue , { concurrency : 5 } ) ) . flat ( ) ; let homepage = null ; let latestStable = null ; const releases = catalogEntries . map ( ( { version , published : releaseTimestamp , projectUrl , listed } ) => { const release = { version : removeBuildMeta ( version ) } ; if ( releaseTimestamp ) { release . releaseTimestamp = releaseTimestamp ; } if ( semver . valid ( version ) && ! semver . prerelease ( version ) ) { latestStable = removeBuildMeta ( version ) ; homepage = projectUrl || homepage ; } if ( listed === false ) { release . isDeprecated = true ; } return release ; } ) ; if ( ! releases . length ) { return null ; } if ( latestStable === null ) { const last = catalogEntries . pop ( ) ; latestStable = removeBuildMeta ( last . version ) ; homepage ??= last . projectUrl ; } const dep = { releases , } ; try { const packageBaseAddress = await getResourceUrl ( registryUrl , 'PackageBaseAddress' ) ; if ( is . nonEmptyString ( packageBaseAddress ) ) { const nuspecUrl = `${ ensureTrailingSlash ( packageBaseAddress ) }${ pkgName . toLowerCase ( ) }/${ latestStable }/${ pkgName . toLowerCase ( ) }.nuspec` ; const metaresult = await http . get ( nuspecUrl ) ; const nuspec = new XmlDocument ( metaresult . body ) ; const sourceUrl = nuspec . valueWithPath ( 'metadata.repository@url' ) ; if ( sourceUrl ) { dep . sourceUrl = sourceUrl ; } } } catch ( err ) { if ( err instanceof ExternalHostError ) { throw err ; } if ( err instanceof HttpError && err . response ?. statusCode === 404 ) { logger . debug ( { registryUrl , pkgName , pkgVersion : latestStable } , `package manifest (.nuspec) not found` ) ; return dep ; } logger . debug ( { err , registryUrl , pkgName , pkgVersion : latestStable } , `Cannot obtain sourceUrl` ) ; return dep ; } if ( homepage ) { dep . sourceUrl ??= homepage ; dep . homepage ??= homepage ; } return dep ; }