0
0
Fork 0
mirror of https://github.com/renovatebot/renovate.git synced 2025-05-12 23:51:55 +00:00
renovatebot_renovate/lib/modules/datasource/npm/get.ts

201 lines
6.6 KiB
TypeScript

import is from '@sindresorhus/is';
import { z } from 'zod';
import { HOST_DISABLED } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import { ExternalHostError } from '../../../types/errors/external-host-error';
import * as hostRules from '../../../util/host-rules';
import type { Http } from '../../../util/http';
import { PackageHttpCacheProvider } from '../../../util/http/cache/package-http-cache-provider';
import type { HttpOptions } from '../../../util/http/types';
import { regEx } from '../../../util/regex';
import { asTimestamp } from '../../../util/timestamp';
import { joinUrlParts } from '../../../util/url';
import type { Release, ReleaseResult } from '../types';
import type { NpmResponse } from './types';
const SHORT_REPO_REGEX = regEx(
/^((?<platform>bitbucket|github|gitlab):)?(?<shortRepo>[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)$/,
);
const platformMapping: Record<string, string> = {
bitbucket: 'https://bitbucket.org/',
github: 'https://github.com/',
gitlab: 'https://gitlab.com/',
};
interface PackageSource {
sourceUrl: string | null;
sourceDirectory: string | null;
}
const PackageSource = z
.union([
z
.string()
.nonempty()
.transform((repository): PackageSource => {
let sourceUrl: string | null = null;
const sourceDirectory = null;
const shortMatch = repository.match(SHORT_REPO_REGEX);
if (shortMatch?.groups) {
const { platform = 'github', shortRepo } = shortMatch.groups;
sourceUrl = platformMapping[platform] + shortRepo;
} else {
sourceUrl = repository;
}
return { sourceUrl, sourceDirectory };
}),
z
.object({
url: z.string().nonempty().nullish(),
directory: z.string().nonempty().nullish(),
})
.transform(({ url, directory }) => {
const res: PackageSource = { sourceUrl: null, sourceDirectory: null };
if (url) {
res.sourceUrl = url;
}
if (directory) {
res.sourceDirectory = directory;
}
return res;
}),
])
.catch({ sourceUrl: null, sourceDirectory: null });
export async function getDependency(
http: Http,
registryUrl: string,
packageName: string,
): Promise<ReleaseResult | null> {
logger.trace(`npm.getDependency(${packageName})`);
const packageUrl = joinUrlParts(registryUrl, packageName.replace('/', '%2F'));
try {
const cacheProvider = new PackageHttpCacheProvider({
namespace: 'datasource-npm:cache-provider',
checkAuthorizationHeader: false, // We don't rely on whether user token is provided or not
checkCacheControlHeader: true,
});
const options: HttpOptions = { cacheProvider };
// set abortOnError for registry.npmjs.org if no hostRule with explicit abortOnError exists
if (
registryUrl === 'https://registry.npmjs.org' &&
hostRules.find({ url: 'https://registry.npmjs.org' })?.abortOnError ===
undefined
) {
logger.trace(
{ packageName, registry: 'https://registry.npmjs.org' },
'setting abortOnError hostRule for well known host',
);
hostRules.add({
matchHost: 'https://registry.npmjs.org',
abortOnError: true,
});
}
const resp = await http.getJsonUnchecked<NpmResponse>(packageUrl, options);
const { body: res } = resp;
if (!res.versions || !Object.keys(res.versions).length) {
// Registry returned a 200 OK but with no versions
logger.debug(`No versions returned for npm dependency ${packageName}`);
return null;
}
const latestVersion = res.versions[res['dist-tags']?.latest ?? ''];
res.repository ??= latestVersion?.repository;
res.homepage ??= latestVersion?.homepage;
const { sourceUrl, sourceDirectory } = PackageSource.parse(res.repository);
// Simplify response before caching and returning
const dep: ReleaseResult = {
homepage: res.homepage,
releases: [],
tags: res['dist-tags'],
registryUrl,
};
if (sourceUrl) {
dep.sourceUrl = sourceUrl;
}
if (sourceDirectory) {
dep.sourceDirectory = sourceDirectory;
}
if (latestVersion?.deprecated) {
dep.deprecationMessage = `On registry \`${registryUrl}\`, the "latest" version of dependency \`${packageName}\` has the following deprecation notice:\n\n\`${latestVersion.deprecated}\`\n\nMarking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.`;
}
dep.releases = Object.keys(res.versions).map((version) => {
const release: Release = {
version,
gitRef: res.versions?.[version].gitHead,
dependencies: res.versions?.[version].dependencies,
devDependencies: res.versions?.[version].devDependencies,
};
const releaseTimestamp = asTimestamp(res.time?.[version]);
if (releaseTimestamp) {
release.releaseTimestamp = releaseTimestamp;
}
if (res.versions?.[version].deprecated) {
release.isDeprecated = true;
}
const nodeConstraint = res.versions?.[version].engines?.node;
if (is.nonEmptyString(nodeConstraint)) {
release.constraints = { node: [nodeConstraint] };
}
const source = PackageSource.parse(res.versions?.[version].repository);
if (source.sourceUrl && source.sourceUrl !== dep.sourceUrl) {
release.sourceUrl = source.sourceUrl;
}
if (
source.sourceDirectory &&
source.sourceDirectory !== dep.sourceDirectory
) {
release.sourceDirectory = source.sourceDirectory;
}
if (dep.deprecationMessage) {
release.isDeprecated = true;
}
return release;
});
const isPublic = resp.headers?.['cache-control']
?.toLocaleLowerCase()
?.split(regEx(/\s*,\s*/))
?.includes('public');
if (!isPublic) {
dep.isPrivate = true;
}
logger.trace({ dep }, 'dep');
return dep;
} catch (err) {
const actualError = err instanceof ExternalHostError ? err.err : err;
const ignoredStatusCodes = [401, 402, 403, 404];
const ignoredResponseCodes = ['ENOTFOUND'];
if (
actualError.message === HOST_DISABLED ||
ignoredStatusCodes.includes(actualError.statusCode) ||
ignoredResponseCodes.includes(actualError.code)
) {
return null;
}
if (err instanceof ExternalHostError) {
if (actualError.name === 'ParseError' && actualError.body) {
actualError.body = 'err.body deleted by Renovate';
err.err = actualError;
}
throw err;
}
logger.debug({ err }, 'Unknown npm lookup error');
return null;
}
}