mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 23:16:26 +00:00
refactor(datasource/nuget): move v2/v3 API logic to classes (#28117)
This commit is contained in:
parent
fde2dff36d
commit
87bba9d31a
5 changed files with 288 additions and 262 deletions
|
@ -1,6 +1,6 @@
|
||||||
import { sortNugetVersions } from './v3';
|
import { sortNugetVersions } from './common';
|
||||||
|
|
||||||
describe('modules/datasource/nuget/v3', () => {
|
describe('modules/datasource/nuget/common', () => {
|
||||||
it.each<{ version: string; other: string; result: number }>`
|
it.each<{ version: string; other: string; result: number }>`
|
||||||
version | other | result
|
version | other | result
|
||||||
${'invalid1'} | ${'invalid2'} | ${0}
|
${'invalid1'} | ${'invalid2'} | ${0}
|
|
@ -1,6 +1,7 @@
|
||||||
import { logger } from '../../../logger';
|
import { logger } from '../../../logger';
|
||||||
import { regEx } from '../../../util/regex';
|
import { regEx } from '../../../util/regex';
|
||||||
import { parseUrl } from '../../../util/url';
|
import { parseUrl } from '../../../util/url';
|
||||||
|
import { api as versioning } from '../../versioning/nuget';
|
||||||
import type { ParsedRegistryUrl } from './types';
|
import type { ParsedRegistryUrl } from './types';
|
||||||
|
|
||||||
const buildMetaRe = regEx(/\+.+$/g);
|
const buildMetaRe = regEx(/\+.+$/g);
|
||||||
|
@ -47,3 +48,23 @@ export function parseRegistryUrl(registryUrl: string): ParsedRegistryUrl {
|
||||||
const feedUrl = parsedUrl.href;
|
const feedUrl = parsedUrl.href;
|
||||||
return { feedUrl, protocolVersion };
|
return { feedUrl, protocolVersion };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two versions. Return:
|
||||||
|
* - `1` if `a > b` or `b` is invalid
|
||||||
|
* - `-1` if `a < b` or `a` is invalid
|
||||||
|
* - `0` if `a == b` or both `a` and `b` are invalid
|
||||||
|
*/
|
||||||
|
export function sortNugetVersions(a: string, b: string): number {
|
||||||
|
if (versioning.isValid(a)) {
|
||||||
|
if (versioning.isValid(b)) {
|
||||||
|
return versioning.sortVersions(a, b);
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else if (versioning.isValid(b)) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ import * as nugetVersioning from '../../versioning/nuget';
|
||||||
import { Datasource } from '../datasource';
|
import { Datasource } from '../datasource';
|
||||||
import type { GetReleasesConfig, ReleaseResult } from '../types';
|
import type { GetReleasesConfig, ReleaseResult } from '../types';
|
||||||
import { parseRegistryUrl } from './common';
|
import { parseRegistryUrl } from './common';
|
||||||
import * as v2 from './v2';
|
import { NugetV2Api } from './v2';
|
||||||
import * as v3 from './v3';
|
import { NugetV3Api } from './v3';
|
||||||
|
|
||||||
// https://api.nuget.org/v3/index.json is a default official nuget feed
|
// https://api.nuget.org/v3/index.json is a default official nuget feed
|
||||||
export const nugetOrg = 'https://api.nuget.org/v3/index.json';
|
export const nugetOrg = 'https://api.nuget.org/v3/index.json';
|
||||||
|
@ -18,6 +18,10 @@ export class NugetDatasource extends Datasource {
|
||||||
|
|
||||||
override readonly registryStrategy = 'merge';
|
override readonly registryStrategy = 'merge';
|
||||||
|
|
||||||
|
readonly v2Api = new NugetV2Api();
|
||||||
|
|
||||||
|
readonly v3Api = new NugetV3Api();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NugetDatasource.id);
|
super(NugetDatasource.id);
|
||||||
}
|
}
|
||||||
|
@ -33,12 +37,17 @@ export class NugetDatasource extends Datasource {
|
||||||
}
|
}
|
||||||
const { feedUrl, protocolVersion } = parseRegistryUrl(registryUrl);
|
const { feedUrl, protocolVersion } = parseRegistryUrl(registryUrl);
|
||||||
if (protocolVersion === 2) {
|
if (protocolVersion === 2) {
|
||||||
return v2.getReleases(this.http, feedUrl, packageName);
|
return this.v2Api.getReleases(this.http, feedUrl, packageName);
|
||||||
}
|
}
|
||||||
if (protocolVersion === 3) {
|
if (protocolVersion === 3) {
|
||||||
const queryUrl = await v3.getResourceUrl(this.http, feedUrl);
|
const queryUrl = await this.v3Api.getResourceUrl(this.http, feedUrl);
|
||||||
if (queryUrl) {
|
if (queryUrl) {
|
||||||
return v3.getReleases(this.http, feedUrl, queryUrl, packageName);
|
return this.v3Api.getReleases(
|
||||||
|
this.http,
|
||||||
|
feedUrl,
|
||||||
|
queryUrl,
|
||||||
|
packageName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,70 +1,74 @@
|
||||||
import { XmlDocument, XmlElement } from 'xmldoc';
|
import { XmlDocument, XmlElement } from 'xmldoc';
|
||||||
import { logger } from '../../../logger';
|
import { logger } from '../../../logger';
|
||||||
import type { Http } from '../../../util/http';
|
import type { Http } from '../../../util/http';
|
||||||
import type { HttpResponse } from '../../../util/http/types';
|
|
||||||
import { regEx } from '../../../util/regex';
|
import { regEx } from '../../../util/regex';
|
||||||
import type { ReleaseResult } from '../types';
|
import type { ReleaseResult } from '../types';
|
||||||
import { massageUrl, removeBuildMeta } from './common';
|
import { massageUrl, removeBuildMeta } from './common';
|
||||||
|
|
||||||
function getPkgProp(pkgInfo: XmlElement, propName: string): string | undefined {
|
export class NugetV2Api {
|
||||||
return pkgInfo.childNamed('m:properties')?.childNamed(`d:${propName}`)?.val;
|
getPkgProp(pkgInfo: XmlElement, propName: string): string | undefined {
|
||||||
}
|
return pkgInfo.childNamed('m:properties')?.childNamed(`d:${propName}`)?.val;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getReleases(
|
async getReleases(
|
||||||
http: Http,
|
http: Http,
|
||||||
feedUrl: string,
|
feedUrl: string,
|
||||||
pkgName: string,
|
pkgName: string,
|
||||||
): Promise<ReleaseResult | null> {
|
): Promise<ReleaseResult | null> {
|
||||||
const dep: ReleaseResult = {
|
const dep: ReleaseResult = {
|
||||||
releases: [],
|
releases: [],
|
||||||
};
|
};
|
||||||
let pkgUrlList: string | null = `${feedUrl.replace(
|
let pkgUrlList: string | null = `${feedUrl.replace(
|
||||||
regEx(/\/+$/),
|
regEx(/\/+$/),
|
||||||
'',
|
'',
|
||||||
)}/FindPackagesById()?id=%27${pkgName}%27&$select=Version,IsLatestVersion,ProjectUrl,Published`;
|
)}/FindPackagesById()?id=%27${pkgName}%27&$select=Version,IsLatestVersion,ProjectUrl,Published`;
|
||||||
while (pkgUrlList !== null) {
|
while (pkgUrlList !== null) {
|
||||||
// typescript issue
|
// typescript issue
|
||||||
const pkgVersionsListRaw: HttpResponse<string> = await http.get(pkgUrlList);
|
const pkgVersionsListRaw = await http.get(pkgUrlList);
|
||||||
const pkgVersionsListDoc = new XmlDocument(pkgVersionsListRaw.body);
|
const pkgVersionsListDoc = new XmlDocument(pkgVersionsListRaw.body);
|
||||||
|
|
||||||
const pkgInfoList = pkgVersionsListDoc.childrenNamed('entry');
|
const pkgInfoList = pkgVersionsListDoc.childrenNamed('entry');
|
||||||
|
|
||||||
for (const pkgInfo of pkgInfoList) {
|
for (const pkgInfo of pkgInfoList) {
|
||||||
const version = getPkgProp(pkgInfo, 'Version');
|
const version = this.getPkgProp(pkgInfo, 'Version');
|
||||||
const releaseTimestamp = getPkgProp(pkgInfo, 'Published');
|
const releaseTimestamp = this.getPkgProp(pkgInfo, 'Published');
|
||||||
dep.releases.push({
|
dep.releases.push({
|
||||||
// TODO: types (#22198)
|
// TODO: types (#22198)
|
||||||
version: removeBuildMeta(`${version}`),
|
version: removeBuildMeta(`${version}`),
|
||||||
releaseTimestamp,
|
releaseTimestamp,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const pkgIsLatestVersion = getPkgProp(pkgInfo, 'IsLatestVersion');
|
const pkgIsLatestVersion = this.getPkgProp(
|
||||||
if (pkgIsLatestVersion === 'true') {
|
pkgInfo,
|
||||||
dep['tags'] = { latest: removeBuildMeta(`${version}`) };
|
'IsLatestVersion',
|
||||||
const projectUrl = getPkgProp(pkgInfo, 'ProjectUrl');
|
);
|
||||||
if (projectUrl) {
|
if (pkgIsLatestVersion === 'true') {
|
||||||
dep.sourceUrl = massageUrl(projectUrl);
|
dep['tags'] = { latest: removeBuildMeta(`${version}`) };
|
||||||
|
const projectUrl = this.getPkgProp(pkgInfo, 'ProjectUrl');
|
||||||
|
if (projectUrl) {
|
||||||
|
dep.sourceUrl = massageUrl(projectUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) /* istanbul ignore next */ {
|
||||||
|
logger.debug(
|
||||||
|
{ err, pkgName, feedUrl },
|
||||||
|
`nuget registry failure: can't parse pkg info for project url`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) /* istanbul ignore next */ {
|
|
||||||
logger.debug(
|
|
||||||
{ err, pkgName, feedUrl },
|
|
||||||
`nuget registry failure: can't parse pkg info for project url`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextPkgUrlListLink = pkgVersionsListDoc
|
||||||
|
.childrenNamed('link')
|
||||||
|
.find((node) => node.attr.rel === 'next');
|
||||||
|
|
||||||
|
pkgUrlList = nextPkgUrlListLink ? nextPkgUrlListLink.attr.href : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPkgUrlListLink = pkgVersionsListDoc
|
// dep not found if no release, so we can try next registry
|
||||||
.childrenNamed('link')
|
if (dep.releases.length === 0) {
|
||||||
.find((node) => node.attr.rel === 'next');
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pkgUrlList = nextPkgUrlListLink ? nextPkgUrlListLink.attr.href : null;
|
return dep;
|
||||||
}
|
}
|
||||||
|
|
||||||
// dep not found if no release, so we can try next registry
|
|
||||||
if (dep.releases.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dep;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { regEx } from '../../../util/regex';
|
||||||
import { ensureTrailingSlash } from '../../../util/url';
|
import { ensureTrailingSlash } from '../../../util/url';
|
||||||
import { api as versioning } from '../../versioning/nuget';
|
import { api as versioning } from '../../versioning/nuget';
|
||||||
import type { Release, ReleaseResult } from '../types';
|
import type { Release, ReleaseResult } from '../types';
|
||||||
import { massageUrl, removeBuildMeta } from './common';
|
import { massageUrl, removeBuildMeta, sortNugetVersions } from './common';
|
||||||
import type {
|
import type {
|
||||||
CatalogEntry,
|
CatalogEntry,
|
||||||
CatalogPage,
|
CatalogPage,
|
||||||
|
@ -18,227 +18,219 @@ import type {
|
||||||
ServicesIndexRaw,
|
ServicesIndexRaw,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const cacheNamespace = 'datasource-nuget';
|
export class NugetV3Api {
|
||||||
|
static readonly cacheNamespace = 'datasource-nuget';
|
||||||
|
|
||||||
export async function getResourceUrl(
|
async getResourceUrl(
|
||||||
http: Http,
|
http: Http,
|
||||||
url: string,
|
url: string,
|
||||||
resourceType = 'RegistrationsBaseUrl',
|
resourceType = 'RegistrationsBaseUrl',
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
// https://docs.microsoft.com/en-us/nuget/api/service-index
|
// https://docs.microsoft.com/en-us/nuget/api/service-index
|
||||||
const resultCacheKey = `${url}:${resourceType}`;
|
const resultCacheKey = `${url}:${resourceType}`;
|
||||||
const cachedResult = await packageCache.get<string>(
|
const cachedResult = await packageCache.get<string>(
|
||||||
cacheNamespace,
|
NugetV3Api.cacheNamespace,
|
||||||
resultCacheKey,
|
resultCacheKey,
|
||||||
);
|
|
||||||
|
|
||||||
// istanbul ignore if
|
|
||||||
if (cachedResult) {
|
|
||||||
return cachedResult;
|
|
||||||
}
|
|
||||||
let servicesIndexRaw: ServicesIndexRaw | undefined;
|
|
||||||
try {
|
|
||||||
const responseCacheKey = url;
|
|
||||||
servicesIndexRaw = await packageCache.get<ServicesIndexRaw>(
|
|
||||||
cacheNamespace,
|
|
||||||
responseCacheKey,
|
|
||||||
);
|
);
|
||||||
// istanbul ignore else: currently not testable
|
|
||||||
if (!servicesIndexRaw) {
|
// istanbul ignore if
|
||||||
servicesIndexRaw = (await http.getJson<ServicesIndexRaw>(url)).body;
|
if (cachedResult) {
|
||||||
await packageCache.set(
|
return cachedResult;
|
||||||
cacheNamespace,
|
|
||||||
responseCacheKey,
|
|
||||||
servicesIndexRaw,
|
|
||||||
3 * 24 * 60,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
let servicesIndexRaw: ServicesIndexRaw | undefined;
|
||||||
const services = servicesIndexRaw.resources
|
try {
|
||||||
.map(({ '@id': serviceId, '@type': t }) => ({
|
const responseCacheKey = url;
|
||||||
serviceId,
|
servicesIndexRaw = await packageCache.get<ServicesIndexRaw>(
|
||||||
type: t?.split('/')?.shift(),
|
NugetV3Api.cacheNamespace,
|
||||||
version: t?.split('/')?.pop(),
|
responseCacheKey,
|
||||||
}))
|
|
||||||
.filter(
|
|
||||||
({ type, version }) => type === resourceType && semver.valid(version),
|
|
||||||
)
|
|
||||||
.sort((x, y) =>
|
|
||||||
x.version && y.version
|
|
||||||
? semver.compare(x.version, y.version)
|
|
||||||
: /* istanbul ignore next: hard to test */ 0,
|
|
||||||
);
|
);
|
||||||
|
// istanbul ignore else: currently not testable
|
||||||
|
if (!servicesIndexRaw) {
|
||||||
|
servicesIndexRaw = (await http.getJson<ServicesIndexRaw>(url)).body;
|
||||||
|
await packageCache.set(
|
||||||
|
NugetV3Api.cacheNamespace,
|
||||||
|
responseCacheKey,
|
||||||
|
servicesIndexRaw,
|
||||||
|
3 * 24 * 60,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (services.length === 0) {
|
const services = servicesIndexRaw.resources
|
||||||
await packageCache.set(cacheNamespace, resultCacheKey, null, 60);
|
.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) =>
|
||||||
|
x.version && y.version
|
||||||
|
? semver.compare(x.version, y.version)
|
||||||
|
: /* istanbul ignore next: hard to test */ 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (services.length === 0) {
|
||||||
|
await packageCache.set(
|
||||||
|
NugetV3Api.cacheNamespace,
|
||||||
|
resultCacheKey,
|
||||||
|
null,
|
||||||
|
60,
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
{ url, servicesIndexRaw },
|
||||||
|
`no ${resourceType} services found`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { serviceId, version } = services.pop()!;
|
||||||
|
|
||||||
|
// istanbul ignore if
|
||||||
|
if (
|
||||||
|
resourceType === 'RegistrationsBaseUrl' &&
|
||||||
|
version &&
|
||||||
|
!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(
|
||||||
|
NugetV3Api.cacheNamespace,
|
||||||
|
resultCacheKey,
|
||||||
|
serviceId,
|
||||||
|
60,
|
||||||
|
);
|
||||||
|
return serviceId;
|
||||||
|
} catch (err) {
|
||||||
|
// istanbul ignore if: not easy testable with nock
|
||||||
|
if (err instanceof ExternalHostError) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ url, servicesIndexRaw },
|
{ err, url, servicesIndexRaw },
|
||||||
`no ${resourceType} services found`,
|
`nuget registry failure: can't get ${resourceType}`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { serviceId, version } = services.pop()!;
|
async getCatalogEntry(
|
||||||
|
http: Http,
|
||||||
|
catalogPage: CatalogPage,
|
||||||
|
): Promise<CatalogEntry[]> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// istanbul ignore if
|
async getReleases(
|
||||||
if (
|
http: Http,
|
||||||
resourceType === 'RegistrationsBaseUrl' &&
|
registryUrl: string,
|
||||||
version &&
|
feedUrl: string,
|
||||||
!version.startsWith('3.0.0-') &&
|
pkgName: string,
|
||||||
!semver.satisfies(version, '^3.0.0')
|
): Promise<ReleaseResult | null> {
|
||||||
) {
|
const baseUrl = feedUrl.replace(regEx(/\/*$/), '');
|
||||||
logger.warn(
|
const url = `${baseUrl}/${pkgName.toLowerCase()}/index.json`;
|
||||||
{ url, version },
|
const packageRegistration = await http.getJson<PackageRegistration>(url);
|
||||||
`Nuget: Unknown version returned. Only v3 is supported`,
|
const catalogPages = packageRegistration.body.items || [];
|
||||||
|
const catalogPagesQueue = catalogPages.map(
|
||||||
|
(page) => (): Promise<CatalogEntry[]> => this.getCatalogEntry(http, page),
|
||||||
|
);
|
||||||
|
const catalogEntries = (await p.all(catalogPagesQueue))
|
||||||
|
.flat()
|
||||||
|
.sort((a, b) => sortNugetVersions(a.version, b.version));
|
||||||
|
|
||||||
|
let homepage: string | null = null;
|
||||||
|
let latestStable: string | null = null;
|
||||||
|
const releases = catalogEntries.map(
|
||||||
|
({ version, published: releaseTimestamp, projectUrl, listed }) => {
|
||||||
|
const release: Release = { version: removeBuildMeta(version) };
|
||||||
|
if (releaseTimestamp) {
|
||||||
|
release.releaseTimestamp = releaseTimestamp;
|
||||||
|
}
|
||||||
|
if (versioning.isValid(version) && versioning.isStable(version)) {
|
||||||
|
latestStable = removeBuildMeta(version);
|
||||||
|
homepage = projectUrl ? massageUrl(projectUrl) : homepage;
|
||||||
|
}
|
||||||
|
if (listed === false) {
|
||||||
|
release.isDeprecated = true;
|
||||||
|
}
|
||||||
|
return release;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!releases.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// istanbul ignore next: only happens when no stable version exists
|
||||||
|
if (latestStable === null && catalogPages.length) {
|
||||||
|
const last = catalogEntries.pop()!;
|
||||||
|
latestStable = removeBuildMeta(last.version);
|
||||||
|
homepage ??= last.projectUrl ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dep: ReleaseResult = {
|
||||||
|
releases,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageBaseAddress = await this.getResourceUrl(
|
||||||
|
http,
|
||||||
|
registryUrl,
|
||||||
|
'PackageBaseAddress',
|
||||||
);
|
);
|
||||||
}
|
// istanbul ignore else: this is a required v3 api
|
||||||
|
if (is.nonEmptyString(packageBaseAddress)) {
|
||||||
await packageCache.set(cacheNamespace, resultCacheKey, serviceId, 60);
|
const nuspecUrl = `${ensureTrailingSlash(
|
||||||
return serviceId;
|
packageBaseAddress,
|
||||||
} catch (err) {
|
)}${pkgName.toLowerCase()}/${
|
||||||
// istanbul ignore if: not easy testable with nock
|
// TODO: types (#22198)
|
||||||
if (err instanceof ExternalHostError) {
|
latestStable
|
||||||
throw err;
|
}/${pkgName.toLowerCase()}.nuspec`;
|
||||||
}
|
const metaresult = await http.get(nuspecUrl);
|
||||||
logger.debug(
|
const nuspec = new XmlDocument(metaresult.body);
|
||||||
{ err, url, servicesIndexRaw },
|
const sourceUrl = nuspec.valueWithPath('metadata.repository@url');
|
||||||
`nuget registry failure: can't get ${resourceType}`,
|
if (sourceUrl) {
|
||||||
);
|
dep.sourceUrl = massageUrl(sourceUrl);
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCatalogEntry(
|
|
||||||
http: Http,
|
|
||||||
catalogPage: CatalogPage,
|
|
||||||
): Promise<CatalogEntry[]> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two versions. Return:
|
|
||||||
* - `1` if `a > b` or `b` is invalid
|
|
||||||
* - `-1` if `a < b` or `a` is invalid
|
|
||||||
* - `0` if `a == b` or both `a` and `b` are invalid
|
|
||||||
*/
|
|
||||||
export function sortNugetVersions(a: string, b: string): number {
|
|
||||||
if (versioning.isValid(a)) {
|
|
||||||
if (versioning.isValid(b)) {
|
|
||||||
return versioning.sortVersions(a, b);
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} else if (versioning.isValid(b)) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getReleases(
|
|
||||||
http: Http,
|
|
||||||
registryUrl: string,
|
|
||||||
feedUrl: string,
|
|
||||||
pkgName: string,
|
|
||||||
): Promise<ReleaseResult | null> {
|
|
||||||
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(http, page),
|
|
||||||
);
|
|
||||||
const catalogEntries = (await p.all(catalogPagesQueue))
|
|
||||||
.flat()
|
|
||||||
.sort((a, b) => sortNugetVersions(a.version, b.version));
|
|
||||||
|
|
||||||
let homepage: string | null = null;
|
|
||||||
let latestStable: string | null = null;
|
|
||||||
const releases = catalogEntries.map(
|
|
||||||
({ version, published: releaseTimestamp, projectUrl, listed }) => {
|
|
||||||
const release: Release = { version: removeBuildMeta(version) };
|
|
||||||
if (releaseTimestamp) {
|
|
||||||
release.releaseTimestamp = releaseTimestamp;
|
|
||||||
}
|
}
|
||||||
if (versioning.isValid(version) && versioning.isStable(version)) {
|
} catch (err) {
|
||||||
latestStable = removeBuildMeta(version);
|
// istanbul ignore if: not easy testable with nock
|
||||||
homepage = projectUrl ? massageUrl(projectUrl) : homepage;
|
if (err instanceof ExternalHostError) {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
if (listed === false) {
|
// ignore / silence 404. Seen on proget, if remote connector is used and package is not yet cached
|
||||||
release.isDeprecated = true;
|
if (err instanceof HttpError && err.response?.statusCode === 404) {
|
||||||
|
logger.debug(
|
||||||
|
{ registryUrl, pkgName, pkgVersion: latestStable },
|
||||||
|
`package manifest (.nuspec) not found`,
|
||||||
|
);
|
||||||
|
return dep;
|
||||||
}
|
}
|
||||||
return release;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!releases.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// istanbul ignore next: only happens when no stable version exists
|
|
||||||
if (latestStable === null && catalogPages.length) {
|
|
||||||
const last = catalogEntries.pop()!;
|
|
||||||
latestStable = removeBuildMeta(last.version);
|
|
||||||
homepage ??= last.projectUrl ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dep: ReleaseResult = {
|
|
||||||
releases,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const packageBaseAddress = await getResourceUrl(
|
|
||||||
http,
|
|
||||||
registryUrl,
|
|
||||||
'PackageBaseAddress',
|
|
||||||
);
|
|
||||||
// istanbul ignore else: this is a required v3 api
|
|
||||||
if (is.nonEmptyString(packageBaseAddress)) {
|
|
||||||
const nuspecUrl = `${ensureTrailingSlash(
|
|
||||||
packageBaseAddress,
|
|
||||||
)}${pkgName.toLowerCase()}/${
|
|
||||||
// TODO: types (#22198)
|
|
||||||
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 = massageUrl(sourceUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// istanbul ignore if: not easy testable with nock
|
|
||||||
if (err instanceof ExternalHostError) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
// ignore / silence 404. Seen on proget, if remote connector is used and package is not yet cached
|
|
||||||
if (err instanceof HttpError && err.response?.statusCode === 404) {
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ registryUrl, pkgName, pkgVersion: latestStable },
|
{ err, registryUrl, pkgName, pkgVersion: latestStable },
|
||||||
`package manifest (.nuspec) not found`,
|
`Cannot obtain sourceUrl`,
|
||||||
);
|
);
|
||||||
return dep;
|
return dep;
|
||||||
}
|
}
|
||||||
logger.debug(
|
|
||||||
{ err, registryUrl, pkgName, pkgVersion: latestStable },
|
// istanbul ignore else: not easy testable
|
||||||
`Cannot obtain sourceUrl`,
|
if (homepage) {
|
||||||
);
|
// only assign if not assigned
|
||||||
|
dep.sourceUrl ??= homepage;
|
||||||
|
dep.homepage ??= homepage;
|
||||||
|
}
|
||||||
|
|
||||||
return dep;
|
return dep;
|
||||||
}
|
}
|
||||||
|
|
||||||
// istanbul ignore else: not easy testable
|
|
||||||
if (homepage) {
|
|
||||||
// only assign if not assigned
|
|
||||||
dep.sourceUrl ??= homepage;
|
|
||||||
dep.homepage ??= homepage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dep;
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue