"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CACHE_REVISION = void 0;
exports.getDependency = getDependency;
const tslib_1 = require("tslib");
const node_url_1 = tslib_1.__importDefault(require("node:url"));
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const luxon_1 = require("luxon");
const zod_1 = require("zod");
const global_1 = require("../../../config/global");
const error_messages_1 = require("../../../constants/error-messages");
const logger_1 = require("../../../logger");
const external_host_error_1 = require("../../../types/errors/external-host-error");
const packageCache = tslib_1.__importStar(require("../../../util/cache/package"));
const hostRules = tslib_1.__importStar(require("../../../util/host-rules"));
const regex_1 = require("../../../util/regex");
const stats_1 = require("../../../util/stats");
const url_1 = require("../../../util/url");
exports.CACHE_REVISION = 1;
const SHORT_REPO_REGEX = (0, regex_1.regEx)(/^((?<platform>bitbucket|github|gitlab):)?(?<shortRepo>[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)$/);
const platformMapping = {
    bitbucket: 'https://bitbucket.org/',
    github: 'https://github.com/',
    gitlab: 'https://gitlab.com/',
};
const PackageSource = zod_1.z
    .union([
    zod_1.z
        .string()
        .nonempty()
        .transform((repository) => {
        let sourceUrl = 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 };
    }),
    zod_1.z
        .object({
        url: zod_1.z.string().nonempty().nullish(),
        directory: zod_1.z.string().nonempty().nullish(),
    })
        .transform(({ url, directory }) => {
        const res = { sourceUrl: null, sourceDirectory: null };
        if (url) {
            res.sourceUrl = url;
        }
        if (directory) {
            res.sourceDirectory = directory;
        }
        return res;
    }),
])
    .catch({ sourceUrl: null, sourceDirectory: null });
async function getDependency(http, registryUrl, packageName) {
    logger_1.logger.trace(`npm.getDependency(${packageName})`);
    const packageUrl = (0, url_1.joinUrlParts)(registryUrl, packageName.replace('/', '%2F'));
    // Now check the persistent cache
    const cacheNamespace = 'datasource-npm:data';
    const cachedResult = await packageCache.get(cacheNamespace, packageUrl);
    if (cachedResult?.cacheData) {
        if (cachedResult.cacheData.revision === exports.CACHE_REVISION) {
            const softExpireAt = luxon_1.DateTime.fromISO(cachedResult.cacheData.softExpireAt);
            if (softExpireAt.isValid && softExpireAt > luxon_1.DateTime.local()) {
                logger_1.logger.trace('Cached result is not expired - reusing');
                stats_1.HttpCacheStats.incLocalHits(packageUrl);
                delete cachedResult.cacheData;
                return cachedResult;
            }
            logger_1.logger.trace('Cached result is soft expired');
            stats_1.HttpCacheStats.incLocalMisses(packageUrl);
        }
        else {
            logger_1.logger.trace(`Package cache for npm package "${packageName}" is from an old revision - discarding`);
            delete cachedResult.cacheData;
        }
    }
    const cacheMinutes = 15;
    const softExpireAt = luxon_1.DateTime.local().plus({ minutes: cacheMinutes }).toISO();
    let cacheHardTtlMinutes = global_1.GlobalConfig.get('cacheHardTtlMinutes');
    if (!(is_1.default.number(cacheHardTtlMinutes) &&
        /* istanbul ignore next: needs test */ cacheHardTtlMinutes > cacheMinutes)) {
        cacheHardTtlMinutes = cacheMinutes;
    }
    const uri = node_url_1.default.parse(packageUrl);
    try {
        const options = {};
        if (cachedResult?.cacheData?.etag) {
            logger_1.logger.trace({ packageName }, 'Using cached etag');
            options.headers = { 'If-None-Match': cachedResult.cacheData.etag };
        }
        // 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_1.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 raw = await http.getJson(packageUrl, options);
        if (cachedResult?.cacheData && raw.statusCode === 304) {
            logger_1.logger.trace(`Cached npm result for ${packageName} is revalidated`);
            stats_1.HttpCacheStats.incRemoteHits(packageUrl);
            cachedResult.cacheData.softExpireAt = softExpireAt;
            await packageCache.set(cacheNamespace, packageUrl, cachedResult, cacheHardTtlMinutes);
            delete cachedResult.cacheData;
            return cachedResult;
        }
        stats_1.HttpCacheStats.incRemoteMisses(packageUrl);
        const etag = raw.headers.etag;
        const res = raw.body;
        if (!res.versions || !Object.keys(res.versions).length) {
            // Registry returned a 200 OK but with no versions
            logger_1.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 = {
            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 = {
                version,
                gitRef: res.versions?.[version].gitHead,
                dependencies: res.versions?.[version].dependencies,
                devDependencies: res.versions?.[version].devDependencies,
            };
            if (res.time?.[version]) {
                release.releaseTimestamp = res.time[version];
            }
            if (res.versions?.[version].deprecated) {
                release.isDeprecated = true;
            }
            const nodeConstraint = res.versions?.[version].engines?.node;
            if (is_1.default.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;
        });
        logger_1.logger.trace({ dep }, 'dep');
        const cacheControl = raw.headers?.['cache-control'];
        if (is_1.default.nonEmptyString(cacheControl) &&
            (0, regex_1.regEx)(/(^|,)\s*public\s*(,|$)/).test(cacheControl)) {
            dep.isPrivate = false;
            const cacheData = { revision: exports.CACHE_REVISION, softExpireAt, etag };
            await packageCache.set(cacheNamespace, packageUrl, { ...dep, cacheData }, etag
                ? /* istanbul ignore next: needs test */ cacheHardTtlMinutes
                : cacheMinutes);
        }
        else {
            dep.isPrivate = true;
        }
        return dep;
    }
    catch (err) {
        const actualError = err instanceof external_host_error_1.ExternalHostError ? err.err : err;
        const ignoredStatusCodes = [401, 402, 403, 404];
        const ignoredResponseCodes = ['ENOTFOUND'];
        if (actualError.message === error_messages_1.HOST_DISABLED ||
            ignoredStatusCodes.includes(actualError.statusCode) ||
            ignoredResponseCodes.includes(actualError.code)) {
            return null;
        }
        if (err instanceof external_host_error_1.ExternalHostError) {
            if (cachedResult) {
                logger_1.logger.warn({ err, host: uri.host }, `npm host error, reusing expired cached result instead`);
                delete cachedResult.cacheData;
                return cachedResult;
            }
            if (actualError.name === 'ParseError' && actualError.body) {
                actualError.body = 'err.body deleted by Renovate';
                err.err = actualError;
            }
            throw err;
        }
        logger_1.logger.debug({ err }, 'Unknown npm lookup error');
        return null;
    }
}
//# sourceMappingURL=get.js.map