"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.divideWorkspaceAndRootDeps = exports.generateLockFile = void 0;
const tslib_1 = require("tslib");
// TODO: types (#22198)
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const semver_1 = tslib_1.__importDefault(require("semver"));
const upath_1 = tslib_1.__importDefault(require("upath"));
const global_1 = require("../../../../config/global");
const error_messages_1 = require("../../../../constants/error-messages");
const logger_1 = require("../../../../logger");
const exec_1 = require("../../../../util/exec");
const fs_1 = require("../../../../util/fs");
const minimatch_1 = require("../../../../util/minimatch");
const result_1 = require("../../../../util/result");
const url_1 = require("../../../../util/url");
const schema_1 = require("../schema");
const utils_1 = require("../utils");
const node_version_1 = require("./node-version");
const utils_2 = require("./utils");
async function getNpmConstraintFromPackageLock(lockFileDir, filename) {
    const packageLockFileName = upath_1.default.join(lockFileDir, filename);
    const packageLockContents = await (0, fs_1.readLocalFile)(packageLockFileName, 'utf8');
    const packageLockJson = result_1.Result.parse(packageLockContents, schema_1.PackageLock).unwrapOrNull();
    if (!packageLockJson) {
        logger_1.logger.debug(`Could not parse ${packageLockFileName}`);
        return null;
    }
    const { lockfileVersion } = packageLockJson;
    if (lockfileVersion === 1) {
        logger_1.logger.debug(`Using npm constraint <7 for lockfileVersion=1`);
        return `<7`;
    }
    if (lockfileVersion === 2) {
        logger_1.logger.debug(`Using npm constraint <9 for lockfileVersion=2`);
        return `<9`;
    }
    logger_1.logger.debug(`Using npm constraint >=9 for lockfileVersion=${lockfileVersion}`);
    return `>=9`;
}
async function generateLockFile(lockFileDir, env, filename, config = {}, upgrades = []) {
    // TODO: don't assume package-lock.json is in the same directory
    const lockFileName = upath_1.default.join(lockFileDir, filename);
    logger_1.logger.debug(`Spawning npm install to create ${lockFileDir}/${filename}`);
    const { skipInstalls, postUpdateOptions } = config;
    let lockFile = null;
    try {
        const lazyPkgJson = (0, utils_2.lazyLoadPackageJson)(lockFileDir);
        const npmToolConstraint = {
            toolName: 'npm',
            constraint: config.constraints?.npm ??
                (0, utils_2.getPackageManagerVersion)('npm', await lazyPkgJson.getValue()) ??
                (await getNpmConstraintFromPackageLock(lockFileDir, filename)) ??
                null,
        };
        const supportsPreferDedupeFlag = !npmToolConstraint.constraint ||
            semver_1.default.intersects('>=7.0.0', npmToolConstraint.constraint);
        const commands = [];
        let cmdOptions = '';
        if ((postUpdateOptions?.includes('npmDedupe') === true &&
            !supportsPreferDedupeFlag) ||
            skipInstalls === false) {
            logger_1.logger.debug('Performing node_modules install');
            cmdOptions += '--no-audit';
        }
        else {
            logger_1.logger.debug('Updating lock file only');
            cmdOptions += '--package-lock-only --no-audit';
        }
        if (postUpdateOptions?.includes('npmDedupe') && supportsPreferDedupeFlag) {
            logger_1.logger.debug('Deduplicate dependencies on installation');
            cmdOptions += ' --prefer-dedupe';
        }
        if (!global_1.GlobalConfig.get('allowScripts') || config.ignoreScripts) {
            cmdOptions += ' --ignore-scripts';
        }
        const extraEnv = {
            NPM_CONFIG_CACHE: env.NPM_CONFIG_CACHE,
            npm_config_store: env.npm_config_store,
        };
        const execOptions = {
            cwdFile: lockFileName,
            userConfiguredEnv: config.env,
            extraEnv,
            toolConstraints: [
                await (0, node_version_1.getNodeToolConstraint)(config, upgrades, lockFileDir, lazyPkgJson),
                npmToolConstraint,
            ],
            docker: {},
        };
        // istanbul ignore if
        if (global_1.GlobalConfig.get('exposeAllEnv')) {
            extraEnv.NPM_AUTH = env.NPM_AUTH;
            extraEnv.NPM_EMAIL = env.NPM_EMAIL;
        }
        if (!upgrades.every((upgrade) => upgrade.isLockfileUpdate)) {
            // This command updates the lock file based on package.json
            commands.push(`npm install ${cmdOptions}`.trim());
        }
        // rangeStrategy = update-lockfile
        const lockUpdates = upgrades.filter((upgrade) => upgrade.isLockfileUpdate);
        // divide the deps in two categories: workspace and root
        const { lockRootUpdates, lockWorkspacesUpdates, workspaces, rootDeps } = divideWorkspaceAndRootDeps(lockFileDir, lockUpdates);
        if (workspaces.size && lockWorkspacesUpdates.length) {
            logger_1.logger.debug('Performing lockfileUpdate (npm-workspaces)');
            for (const workspace of workspaces) {
                const currentWorkspaceUpdates = lockWorkspacesUpdates
                    .filter((update) => update.workspace === workspace)
                    .map((update) => update.managerData?.packageKey)
                    .filter((packageKey) => !rootDeps.has(packageKey));
                if (currentWorkspaceUpdates.length) {
                    const updateCmd = `npm install ${cmdOptions} --workspace=${workspace} ${currentWorkspaceUpdates.join(' ')}`;
                    commands.push(updateCmd);
                }
            }
        }
        if (lockRootUpdates.length) {
            logger_1.logger.debug('Performing lockfileUpdate (npm)');
            const updateCmd = `npm install ${cmdOptions} ` +
                lockRootUpdates
                    .map((update) => update.managerData?.packageKey)
                    .join(' ');
            commands.push(updateCmd);
        }
        if (upgrades.some((upgrade) => upgrade.isRemediation)) {
            // We need to run twice to get the correct lock file
            commands.push(`npm install ${cmdOptions}`.trim());
        }
        // postUpdateOptions
        if (config.postUpdateOptions?.includes('npmDedupe') &&
            !supportsPreferDedupeFlag) {
            logger_1.logger.debug('Performing npm dedupe after installation');
            commands.push('npm dedupe');
        }
        if (upgrades.find((upgrade) => upgrade.isLockFileMaintenance)) {
            logger_1.logger.debug(`Removing ${lockFileName} first due to lock file maintenance upgrade`);
            try {
                await (0, fs_1.deleteLocalFile)(lockFileName);
            }
            catch (err) /* istanbul ignore next */ {
                logger_1.logger.debug({ err, lockFileName }, 'Error removing package-lock.json for lock file maintenance');
            }
        }
        // Run the commands
        await (0, exec_1.exec)(commands, execOptions);
        // massage to shrinkwrap if necessary
        if (filename === 'npm-shrinkwrap.json' &&
            (await (0, fs_1.localPathExists)(upath_1.default.join(lockFileDir, 'package-lock.json')))) {
            await (0, fs_1.renameLocalFile)(upath_1.default.join(lockFileDir, 'package-lock.json'), upath_1.default.join(lockFileDir, 'npm-shrinkwrap.json'));
        }
        // Read the result
        // TODO #22198
        lockFile = (await (0, fs_1.readLocalFile)(upath_1.default.join(lockFileDir, filename), 'utf8'));
        // Massage lockfile counterparts of package.json that were modified
        // because npm install was called with an explicit version for rangeStrategy=update-lockfile
        if (lockUpdates.length) {
            const { detectedIndent, lockFileParsed } = (0, utils_1.parseLockFile)(lockFile);
            if (lockFileParsed?.lockfileVersion === 2 ||
                lockFileParsed?.lockfileVersion === 3) {
                lockUpdates.forEach((lockUpdate) => {
                    const depType = lockUpdate.depType;
                    // TODO #22198
                    if (lockFileParsed.packages?.['']?.[depType]?.[lockUpdate.packageName]) {
                        lockFileParsed.packages[''][depType][lockUpdate.packageName] =
                            lockUpdate.newValue;
                    }
                });
                lockFile = (0, utils_1.composeLockFile)(lockFileParsed, detectedIndent);
            }
        }
    }
    catch (err) /* istanbul ignore next */ {
        if (err.message === error_messages_1.TEMPORARY_ERROR) {
            throw err;
        }
        logger_1.logger.debug({
            err,
            type: 'npm',
        }, 'lock file error');
        if (err.stderr?.includes('ENOSPC: no space left on device')) {
            throw new Error(error_messages_1.SYSTEM_INSUFFICIENT_DISK_SPACE);
        }
        return { error: true, stderr: err.stderr };
    }
    return { error: !lockFile, lockFile };
}
exports.generateLockFile = generateLockFile;
function divideWorkspaceAndRootDeps(lockFileDir, lockUpdates) {
    const lockRootUpdates = []; // stores all upgrades which are present in root package.json
    const lockWorkspacesUpdates = []; // stores all upgrades which are present in workspaces package.json
    const workspaces = new Set(); // name of all workspaces
    const rootDeps = new Set(); // packageName of all upgrades in root package.json (makes it check duplicate deps in root)
    // divide the deps in two categories: workspace and root
    for (const upgrade of lockUpdates) {
        upgrade.managerData ??= {};
        upgrade.managerData.packageKey = generatePackageKey(upgrade.packageName, upgrade.newVersion);
        if (upgrade.managerData.workspacesPackages?.length &&
            is_1.default.string(upgrade.packageFile)) {
            const workspacePatterns = upgrade.managerData.workspacesPackages; // glob pattern or directory name/path
            const packageFileDir = (0, url_1.trimSlashes)(upgrade.packageFile.replace('package.json', ''));
            // workspaceDir = packageFileDir - lockFileDir
            const workspaceDir = (0, url_1.trimSlashes)(packageFileDir.replace(lockFileDir, ''));
            if (is_1.default.nonEmptyString(workspaceDir)) {
                let workspaceName;
                // compare workspaceDir to workspace patterns
                // stop when the first match is found and
                // add workspaceDir to workspaces set and upgrade object
                for (const workspacePattern of workspacePatterns) {
                    const massagedPattern = workspacePattern.replace(/^\.\//, '');
                    if ((0, minimatch_1.minimatch)(massagedPattern).match(workspaceDir)) {
                        workspaceName = workspaceDir;
                        break;
                    }
                }
                if (workspaceName) {
                    if (!rootDeps.has(upgrade.managerData.packageKey) // prevent same dep from existing in root and workspace
                    ) {
                        workspaces.add(workspaceName);
                        upgrade.workspace = workspaceName;
                        lockWorkspacesUpdates.push(upgrade);
                    }
                }
                else {
                    logger_1.logger.warn({ workspacePatterns, workspaceDir }, 'workspaceDir not found');
                }
                continue;
            }
        }
        lockRootUpdates.push(upgrade);
        rootDeps.add(upgrade.managerData.packageKey);
    }
    return { lockRootUpdates, lockWorkspacesUpdates, workspaces, rootDeps };
}
exports.divideWorkspaceAndRootDeps = divideWorkspaceAndRootDeps;
function generatePackageKey(packageName, version) {
    return `${packageName}@${version}`;
}
//# sourceMappingURL=npm.js.map