"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDependency = void 0;
const tslib_1 = require("tslib");
const url_1 = tslib_1.__importDefault(require("url"));
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const luxon_1 = require("luxon");
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 regex_1 = require("../../../util/regex");
const url_2 = require("../../../util/url");
const common_1 = require("./common");
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/',
};
function getPackageSource(repository) {
    const res = {};
    if (repository) {
        if (is_1.default.nonEmptyString(repository)) {
            const shortMatch = repository.match(SHORT_REPO_REGEX);
            if (shortMatch?.groups) {
                const { platform = 'github', shortRepo } = shortMatch.groups;
                res.sourceUrl = platformMapping[platform] + shortRepo;
            }
            else {
                res.sourceUrl = repository;
            }
        }
        else if (is_1.default.nonEmptyString(repository.url)) {
            res.sourceUrl = repository.url;
        }
        if (is_1.default.nonEmptyString(repository.directory)) {
            res.sourceDirectory = repository.directory;
        }
    }
    return res;
}
async function getDependency(http, registryUrl, packageName) {
    logger_1.logger.trace(`npm.getDependency(${packageName})`);
    const packageUrl = (0, url_2.joinUrlParts)(registryUrl, packageName.replace('/', '%2F'));
    // Now check the persistent cache
    const cacheNamespace = 'datasource-npm';
    const cachedResult = await packageCache.get(cacheNamespace, packageUrl);
    if (cachedResult) {
        if (cachedResult.cacheData) {
            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');
                delete cachedResult.cacheData;
                return cachedResult;
            }
            logger_1.logger.trace('Cached result is soft expired');
        }
        else {
            logger_1.logger.trace('Reusing legacy cached result');
            return cachedResult;
        }
    }
    const cacheMinutes = process.env.RENOVATE_CACHE_NPM_MINUTES
        ? parseInt(process.env.RENOVATE_CACHE_NPM_MINUTES, 10)
        : 15;
    const softExpireAt = luxon_1.DateTime.local().plus({ minutes: cacheMinutes }).toISO();
    let { cacheHardTtlMinutes } = global_1.GlobalConfig.get();
    if (!(is_1.default.number(cacheHardTtlMinutes) && cacheHardTtlMinutes > cacheMinutes)) {
        cacheHardTtlMinutes = cacheMinutes;
    }
    const uri = url_1.default.parse(packageUrl);
    try {
        const options = {};
        if (cachedResult?.cacheData?.etag) {
            logger_1.logger.debug('Using cached etag');
            options.headers = { 'If-None-Match': cachedResult.cacheData.etag };
        }
        const raw = await http.getJson(packageUrl, options);
        if (cachedResult?.cacheData && raw.statusCode === 304) {
            logger_1.logger.debug({ packageName }, 'Cached npm result is revalidated');
            cachedResult.cacheData.softExpireAt = softExpireAt;
            await packageCache.set(cacheNamespace, packageUrl, cachedResult, cacheHardTtlMinutes);
            delete cachedResult.cacheData;
            return cachedResult;
        }
        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 ?? (res.repository = latestVersion?.repository);
        res.homepage ?? (res.homepage = latestVersion?.homepage);
        const { sourceUrl, sourceDirectory } = getPackageSource(res.repository);
        // Simplify response before caching and returning
        const dep = {
            name: res.name,
            homepage: res.homepage,
            sourceUrl,
            sourceDirectory,
            versions: {},
            releases: [],
            'dist-tags': res['dist-tags'],
            registryUrl,
        };
        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.deprecationSource = common_1.id;
        }
        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 source = getPackageSource(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;
            }
            return release;
        });
        logger_1.logger.trace({ dep }, 'dep');
        // serialize first before saving
        // TODO: use dynamic detection of public repos instead of a static list (#9587)
        const whitelistedPublicScopes = [
            '@graphql-codegen',
            '@storybook',
            '@types',
            '@typescript-eslint',
        ];
        if (!raw.authorization &&
            (whitelistedPublicScopes.includes(packageName.split('/')[0]) ||
                !packageName.startsWith('@'))) {
            const cacheData = { softExpireAt, etag };
            await packageCache.set(cacheNamespace, packageUrl, { ...dep, cacheData }, etag ? cacheHardTtlMinutes : cacheMinutes);
        }
        return dep;
    }
    catch (err) {
        const ignoredStatusCodes = [401, 402, 403, 404];
        const ignoredResponseCodes = ['ENOTFOUND'];
        if (err.message === error_messages_1.HOST_DISABLED ||
            ignoredStatusCodes.includes(err.statusCode) ||
            ignoredResponseCodes.includes(err.code)) {
            return null;
        }
        if (uri.host === 'registry.npmjs.org') {
            if (cachedResult) {
                logger_1.logger.warn({ err }, 'npmjs error, reusing expired cached result instead');
                delete cachedResult.cacheData;
                return cachedResult;
            }
            // istanbul ignore if
            if (err.name === 'ParseError' && err.body) {
                err.body = 'err.body deleted by Renovate';
            }
            throw new external_host_error_1.ExternalHostError(err);
        }
        logger_1.logger.debug({ err }, 'Unknown npm lookup error');
        return null;
    }
}
exports.getDependency = getDependency;
//# sourceMappingURL=get.js.map