"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("@pnpm/constants");
const core_loggers_1 = require("@pnpm/core-loggers");
const headless_1 = require("@pnpm/headless");
const lifecycle_1 = require("@pnpm/lifecycle");
const lockfile_file_1 = require("@pnpm/lockfile-file");
const lockfile_utils_1 = require("@pnpm/lockfile-utils");
const logger_1 = require("@pnpm/logger");
const modules_yaml_1 = require("@pnpm/modules-yaml");
const read_modules_dir_1 = require("@pnpm/read-modules-dir");
const resolve_dependencies_1 = require("@pnpm/resolve-dependencies");
const types_1 = require("@pnpm/types");
const utils_1 = require("@pnpm/utils");
const dp = require("dependency-path");
const graphSequencer = require("graph-sequencer");
const isInnerLink = require("is-inner-link");
const isSubdir = require("is-subdir");
const p_every_1 = require("p-every");
const pFilter = require("p-filter");
const path = require("path");
const R = require("ramda");
const rimraf = require("rimraf-then");
const run_groups_1 = require("run-groups");
const semver = require("semver");
const errorTypes_1 = require("../errorTypes");
const getContext_1 = require("../getContext");
const getSpecFromPackageJson_1 = require("../getSpecFromPackageJson");
const lock_1 = require("../lock");
const lockfilesEqual_1 = require("../lockfilesEqual");
const parseWantedDependencies_1 = require("../parseWantedDependencies");
const safeIsInnerLink_1 = require("../safeIsInnerLink");
const save_1 = require("../save");
const removeDeps_1 = require("../uninstall/removeDeps");
const getPref_1 = require("../utils/getPref");
const extendInstallOptions_1 = require("./extendInstallOptions");
const link_1 = require("./link");
const lockfile_1 = require("./lockfile");
function install(opts) {
    return mutateModules([
        {
            buildIndex: 0,
            mutation: 'install',
            prefix: opts.prefix || process.cwd(),
        },
    ], opts);
}
exports.install = install;
async function mutateModules(importers, maybeOpts) {
    const reporter = maybeOpts && maybeOpts.reporter;
    if (reporter) {
        logger_1.streamParser.on('data', reporter);
    }
    const opts = await extendInstallOptions_1.default(maybeOpts);
    if (!opts.include.dependencies && opts.include.optionalDependencies) {
        throw new errorTypes_1.PnpmError('ERR_PNPM_OPTIONAL_DEPS_REQUIRE_PROD_DEPS', 'Optional dependencies cannot be installed without production dependencies');
    }
    const ctx = await getContext_1.default(importers, opts);
    for (const importer of ctx.importers) {
        if (!importer.pkg) {
            throw new Error(`No package.json found in "${importer.prefix}"`);
        }
    }
    if (opts.lock) {
        await lock_1.default(ctx.lockfileDirectory, _install, {
            locks: opts.locks,
            prefix: ctx.lockfileDirectory,
            stale: opts.lockStaleDuration,
            storeController: opts.storeController,
        });
    }
    else {
        await _install();
    }
    if (reporter) {
        logger_1.streamParser.removeListener('data', reporter);
    }
    async function _install() {
        const installsOnly = importers.every((importer) => importer.mutation === 'install');
        if (!opts.lockfileOnly &&
            !opts.update &&
            installsOnly &&
            (opts.frozenLockfile ||
                opts.preferFrozenLockfile &&
                    (!opts.pruneLockfileImporters || Object.keys(ctx.wantedLockfile.importers).length === ctx.importers.length) &&
                    ctx.existsWantedLockfile &&
                    ctx.wantedLockfile.lockfileVersion === constants_1.LOCKFILE_VERSION &&
                    await p_every_1.default(ctx.importers, async (importer) => !hasLocalTarballDepsInRoot(ctx.wantedLockfile, importer.id) &&
                        lockfile_utils_1.satisfiesPackageJson(ctx.wantedLockfile, importer.pkg, importer.id) &&
                        linkedPackagesAreUpToDate(importer.pkg, ctx.wantedLockfile.importers[importer.id], importer.prefix, opts.localPackages)))) {
            if (!ctx.existsWantedLockfile) {
                if (ctx.importers.some((importer) => pkgHasDependencies(importer.pkg))) {
                    throw new Error(`Headless installation requires a ${constants_1.WANTED_LOCKFILE} file`);
                }
            }
            else {
                logger_1.default.info({ message: 'Performing headless installation', prefix: opts.lockfileDirectory });
                await headless_1.default({
                    currentEngine: {
                        nodeVersion: opts.nodeVersion,
                        pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '',
                    },
                    currentLockfile: ctx.currentLockfile,
                    engineStrict: opts.engineStrict,
                    force: opts.force,
                    ignoreScripts: opts.ignoreScripts,
                    importers: ctx.importers,
                    include: opts.include,
                    independentLeaves: opts.independentLeaves,
                    lockfileDirectory: ctx.lockfileDirectory,
                    ownLifecycleHooksStdio: opts.ownLifecycleHooksStdio,
                    packageManager: opts.packageManager,
                    pendingBuilds: ctx.pendingBuilds,
                    pruneStore: opts.pruneStore,
                    rawNpmConfig: opts.rawNpmConfig,
                    registries: opts.registries,
                    sideEffectsCacheRead: opts.sideEffectsCacheRead,
                    sideEffectsCacheWrite: opts.sideEffectsCacheWrite,
                    skipped: ctx.skipped,
                    store: opts.store,
                    storeController: opts.storeController,
                    unsafePerm: opts.unsafePerm,
                    userAgent: opts.userAgent,
                    wantedLockfile: ctx.wantedLockfile,
                });
                return;
            }
        }
        const importersToInstall = [];
        const importersToBeInstalled = ctx.importers.filter((importer) => importer.mutation === 'install');
        const scriptsOpts = {
            rawNpmConfig: opts.rawNpmConfig,
            stdio: opts.ownLifecycleHooksStdio,
            unsafePerm: opts.unsafePerm || false,
        };
        if (!opts.ignoreScripts) {
            await lifecycle_1.runLifecycleHooksConcurrently(['preinstall'], importersToBeInstalled, opts.childConcurrency, scriptsOpts);
        }
        // TODO: make it concurrent
        for (const importer of ctx.importers) {
            switch (importer.mutation) {
                case 'uninstallSome':
                    importersToInstall.push(Object.assign({ pruneDirectDependencies: false }, importer, { linkedPackages: [], newPkgRawSpecs: [], nonLinkedPackages: [], removePackages: importer.dependencyNames, updatePackageJson: true, usesExternalLockfile: ctx.lockfileDirectory !== importer.prefix, wantedDeps: [] }));
                    break;
                case 'install': {
                    await installCase(importer);
                    break;
                }
                case 'installSome': {
                    const currentPrefs = opts.ignoreCurrentPrefs ? {} : utils_1.getAllDependenciesFromPackage(importer.pkg);
                    const optionalDependencies = importer.targetDependenciesField ? {} : importer.pkg.optionalDependencies || {};
                    const devDependencies = importer.targetDependenciesField ? {} : importer.pkg.devDependencies || {};
                    const wantedDeps = parseWantedDependencies_1.default(importer.dependencySelectors, {
                        allowNew: importer.allowNew !== false,
                        currentPrefs,
                        defaultTag: opts.tag,
                        dev: importer.targetDependenciesField === 'devDependencies',
                        devDependencies,
                        optional: importer.targetDependenciesField === 'optionalDependencies',
                        optionalDependencies,
                    });
                    importersToInstall.push(Object.assign({ pruneDirectDependencies: false }, importer, { linkedPackages: [], newPkgRawSpecs: wantedDeps.map((wantedDependency) => wantedDependency.raw), nonLinkedPackages: wantedDeps, updatePackageJson: true, usesExternalLockfile: ctx.lockfileDirectory !== importer.prefix, wantedDeps }));
                    break;
                }
                case 'unlink': {
                    const packageDirs = await read_modules_dir_1.default(importer.modulesDir);
                    const externalPackages = await pFilter(packageDirs, (packageDir) => isExternalLink(ctx.storePath, importer.modulesDir, packageDir));
                    const allDeps = utils_1.getAllDependenciesFromPackage(importer.pkg);
                    const packagesToInstall = [];
                    for (const pkgName of externalPackages) {
                        await rimraf(path.join(importer.modulesDir, pkgName));
                        if (allDeps[pkgName]) {
                            packagesToInstall.push(pkgName);
                        }
                    }
                    if (!packagesToInstall.length)
                        return;
                    // TODO: install only those that were unlinked
                    // but don't update their version specs in package.json
                    await installCase(Object.assign({}, importer, { mutation: 'install' }));
                    break;
                }
                case 'unlinkSome': {
                    const packagesToInstall = [];
                    const allDeps = utils_1.getAllDependenciesFromPackage(importer.pkg);
                    for (const depName of importer.dependencyNames) {
                        try {
                            if (!await isExternalLink(ctx.storePath, importer.modulesDir, depName)) {
                                logger_1.default.warn({
                                    message: `${depName} is not an external link`,
                                    prefix: importer.prefix,
                                });
                                continue;
                            }
                        }
                        catch (err) {
                            if (err['code'] !== 'ENOENT')
                                throw err; // tslint:disable-line:no-string-literal
                        }
                        await rimraf(path.join(importer.modulesDir, depName));
                        if (allDeps[depName]) {
                            packagesToInstall.push(depName);
                        }
                    }
                    if (!packagesToInstall.length)
                        return;
                    // TODO: install only those that were unlinked
                    // but don't update their version specs in package.json
                    await installCase(Object.assign({}, importer, { mutation: 'install' }));
                    break;
                }
            }
        }
        async function installCase(importer) {
            const wantedDeps = utils_1.getWantedDependencies(importer.pkg);
            if (ctx.wantedLockfile && ctx.wantedLockfile.importers) {
                forgetResolutionsOfPrevWantedDeps(ctx.wantedLockfile.importers[importer.id], wantedDeps);
            }
            const scripts = !opts.ignoreScripts && importer.pkg && importer.pkg.scripts || {};
            if (opts.ignoreScripts && importer.pkg && importer.pkg.scripts &&
                (importer.pkg.scripts.preinstall || importer.pkg.scripts.prepublish ||
                    importer.pkg.scripts.install ||
                    importer.pkg.scripts.postinstall ||
                    importer.pkg.scripts.prepare)) {
                ctx.pendingBuilds.push(importer.id);
            }
            if (scripts['prepublish']) { // tslint:disable-line:no-string-literal
                logger_1.default.warn({
                    message: '`prepublish` scripts are deprecated. Use `prepare` for build steps and `prepublishOnly` for upload-only.',
                    prefix: importer.prefix,
                });
            }
            importersToInstall.push(Object.assign({ pruneDirectDependencies: false }, importer, await partitionLinkedPackages(wantedDeps, {
                localPackages: opts.localPackages,
                lockfileOnly: opts.lockfileOnly,
                modulesDir: importer.modulesDir,
                prefix: importer.prefix,
                storePath: ctx.storePath,
                virtualStoreDir: ctx.virtualStoreDir,
            }), { newPkgRawSpecs: [], updatePackageJson: false, usesExternalLockfile: ctx.lockfileDirectory !== importer.prefix, wantedDeps }));
        }
        // Unfortunately, the private lockfile may differ from the public one.
        // A user might run named installations on a project that has a pnpm-lock.yaml file before running a noop install
        const makePartialCurrentLockfile = !installsOnly && (ctx.existsWantedLockfile && !ctx.existsCurrentLockfile ||
            // TODO: this operation is quite expensive. We'll have to find a better solution to do this.
            // maybe in pnpm v2 it won't be needed. See: https://github.com/pnpm/pnpm/issues/841
            !lockfilesEqual_1.default(ctx.currentLockfile, ctx.wantedLockfile));
        await installInContext(importersToInstall, ctx, Object.assign({}, opts, { makePartialCurrentLockfile, update: opts.update || !installsOnly, updateLockfileMinorVersion: true }));
        if (!opts.ignoreScripts) {
            await lifecycle_1.runLifecycleHooksConcurrently(['install', 'postinstall', 'prepublish', 'prepare'], importersToBeInstalled, opts.childConcurrency, scriptsOpts);
        }
    }
}
exports.mutateModules = mutateModules;
async function isExternalLink(store, modules, pkgName) {
    const link = await isInnerLink(modules, pkgName);
    // checking whether the link is pointing to the store is needed
    // because packages are linked to store when independent-leaves = true
    return !link.isInner && !isSubdir(store, link.target);
}
function pkgHasDependencies(pkg) {
    return Boolean(R.keys(pkg.dependencies).length ||
        R.keys(pkg.devDependencies).length ||
        R.keys(pkg.optionalDependencies).length);
}
async function partitionLinkedPackages(wantedDeps, opts) {
    const nonLinkedPackages = [];
    const linkedPackages = [];
    for (const wantedDependency of wantedDeps) {
        if (!wantedDependency.alias || opts.localPackages && opts.localPackages[wantedDependency.alias]) {
            nonLinkedPackages.push(wantedDependency);
            continue;
        }
        const isInnerLink = await safeIsInnerLink_1.default(opts.virtualStoreDir, wantedDependency.alias, {
            hideAlienModules: opts.lockfileOnly === false,
            prefix: opts.prefix,
            storePath: opts.storePath,
        });
        if (isInnerLink === true) {
            nonLinkedPackages.push(wantedDependency);
            continue;
        }
        // This info-log might be better to be moved to the reporter
        logger_1.default.info({
            message: `${wantedDependency.alias} is linked to ${opts.modulesDir} from ${isInnerLink}`,
            prefix: opts.prefix,
        });
        linkedPackages.push(wantedDependency);
    }
    return {
        linkedPackages,
        nonLinkedPackages,
    };
}
// If the specifier is new, the old resolution probably does not satisfy it anymore.
// By removing these resolutions we ensure that they are resolved again using the new specs.
function forgetResolutionsOfPrevWantedDeps(importer, wantedDeps) {
    if (!importer.specifiers)
        return;
    importer.dependencies = importer.dependencies || {};
    importer.devDependencies = importer.devDependencies || {};
    importer.optionalDependencies = importer.optionalDependencies || {};
    for (const wantedDep of wantedDeps) {
        if (wantedDep.alias && importer.specifiers[wantedDep.alias] !== wantedDep.pref) {
            if (importer.dependencies[wantedDep.alias] && !importer.dependencies[wantedDep.alias].startsWith('link:')) {
                delete importer.dependencies[wantedDep.alias];
            }
            delete importer.devDependencies[wantedDep.alias];
            delete importer.optionalDependencies[wantedDep.alias];
        }
    }
}
async function linkedPackagesAreUpToDate(pkg, lockfileImporter, prefix, localPackages) {
    const localPackagesByDirectory = localPackages ? getLocalPackagesByDirectory(localPackages) : {};
    for (const depField of types_1.DEPENDENCIES_FIELDS) {
        const importerDeps = lockfileImporter[depField];
        const pkgDeps = pkg[depField];
        if (!importerDeps || !pkgDeps)
            continue;
        const depNames = Object.keys(importerDeps);
        for (const depName of depNames) {
            if (!pkgDeps[depName])
                continue;
            const isLinked = importerDeps[depName].startsWith('link:');
            if (isLinked && (pkgDeps[depName].startsWith('link:') || pkgDeps[depName].startsWith('file:')))
                continue;
            const dir = isLinked
                ? path.join(prefix, importerDeps[depName].substr(5))
                : (localPackages && localPackages[depName] && localPackages[depName] && localPackages[depName][importerDeps[depName]] && localPackages[depName][importerDeps[depName]].directory);
            if (!dir)
                continue;
            const linkedPkg = localPackagesByDirectory[dir] || await utils_1.safeReadPackageFromDir(dir);
            const localPackageSatisfiesRange = linkedPkg && semver.satisfies(linkedPkg.version, pkgDeps[depName]);
            if (isLinked !== localPackageSatisfiesRange)
                return false;
        }
    }
    return true;
}
function getLocalPackagesByDirectory(localPackages) {
    const localPackagesByDirectory = {};
    Object.keys(localPackages || {}).forEach((pkgName) => {
        Object.keys(localPackages[pkgName] || {}).forEach((pkgVersion) => {
            localPackagesByDirectory[localPackages[pkgName][pkgVersion].directory] = localPackages[pkgName][pkgVersion].package;
        });
    });
    return localPackagesByDirectory;
}
function hasLocalTarballDepsInRoot(lockfile, importerId) {
    const importer = lockfile.importers && lockfile.importers[importerId];
    if (!importer)
        return false;
    return R.any(refIsLocalTarball, R.values(importer.dependencies || {}))
        || R.any(refIsLocalTarball, R.values(importer.devDependencies || {}))
        || R.any(refIsLocalTarball, R.values(importer.optionalDependencies || {}));
}
function refIsLocalTarball(ref) {
    return ref.startsWith('file:') && (ref.endsWith('.tgz') || ref.endsWith('.tar.gz') || ref.endsWith('.tar'));
}
async function addDependenciesToPackage(dependencySelectors, opts) {
    return mutateModules([
        {
            allowNew: opts.allowNew,
            dependencySelectors,
            mutation: 'installSome',
            pinnedVersion: opts.pinnedVersion,
            prefix: opts.prefix || process.cwd(),
            shamefullyFlatten: opts.shamefullyFlatten,
            targetDependenciesField: opts.targetDependenciesField,
        },
    ], Object.assign({}, opts, { lockfileDirectory: opts.lockfileDirectory || opts.prefix }));
}
exports.addDependenciesToPackage = addDependenciesToPackage;
async function installInContext(importers, ctx, opts) {
    if (opts.lockfileOnly && ctx.existsCurrentLockfile) {
        logger_1.default.warn({
            message: '`node_modules` is present. Lockfile only installation will make it out-of-date',
            prefix: ctx.lockfileDirectory,
        });
    }
    // Avoid requesting package meta info from registry only when the lockfile version is at least the expected
    const hasManifestInLockfile = ctx.wantedLockfile.lockfileVersion >= constants_1.LOCKFILE_VERSION;
    ctx.wantedLockfile.importers = ctx.wantedLockfile.importers || {};
    for (const importer of importers) {
        if (!ctx.wantedLockfile.importers[importer.id]) {
            ctx.wantedLockfile.importers[importer.id] = { specifiers: {} };
        }
    }
    if (opts.pruneLockfileImporters) {
        const importerIds = new Set(importers.map((importer) => importer.id));
        for (const wantedImporter of Object.keys(ctx.wantedLockfile.importers)) {
            if (!importerIds.has(wantedImporter)) {
                delete ctx.wantedLockfile.importers[wantedImporter];
            }
        }
    }
    await Promise.all(importers
        .map(async (importer) => {
        if (importer.mutation !== 'uninstallSome')
            return;
        const pkgJsonPath = path.join(importer.prefix, 'package.json');
        importer.pkg = await removeDeps_1.default(pkgJsonPath, importer.dependencyNames, {
            prefix: importer.prefix,
            saveType: importer.targetDependenciesField,
        });
    }));
    core_loggers_1.stageLogger.debug({
        prefix: ctx.lockfileDirectory,
        stage: 'resolution_started',
    });
    const { dependenciesTree, outdatedDependencies, resolvedImporters, resolvedPackagesByPackageId, wantedToBeSkippedPackageIds, } = await resolve_dependencies_1.default({
        currentLockfile: ctx.currentLockfile,
        dryRun: opts.lockfileOnly,
        engineStrict: opts.engineStrict,
        force: opts.force,
        hasManifestInLockfile,
        hooks: opts.hooks,
        importers,
        localPackages: opts.localPackages,
        lockfileDirectory: opts.lockfileDirectory,
        nodeVersion: opts.nodeVersion,
        pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '',
        preferredVersions: opts.preferredVersions,
        registries: opts.registries,
        resolutionStrategy: opts.resolutionStrategy,
        sideEffectsCache: opts.sideEffectsCacheRead,
        storeController: opts.storeController,
        tag: opts.tag,
        updateDepth: (() => {
            // This can be remove from lockfile v4
            if (!hasManifestInLockfile) {
                // The lockfile has to be updated to contain
                // the necessary info from package manifests
                return Infinity;
            }
            if (opts.force)
                return Infinity;
            if (opts.update) {
                return opts.depth;
            }
            if (modulesIsUpToDate({
                currentLockfile: ctx.currentLockfile,
                defaultRegistry: ctx.registries.default,
                skippedRelDepPaths: Array.from(ctx.skipped),
                wantedLockfile: ctx.wantedLockfile,
            })) {
                return opts.repeatInstallDepth;
            }
            return Infinity;
        })(),
        virtualStoreDir: ctx.virtualStoreDir,
        wantedLockfile: ctx.wantedLockfile,
    });
    core_loggers_1.stageLogger.debug({
        prefix: ctx.lockfileDirectory,
        stage: 'resolution_done',
    });
    const importersToLink = await Promise.all(importers.map(async (importer) => {
        const resolvedImporter = resolvedImporters[importer.id];
        let newPkg = importer.pkg;
        if (importer.updatePackageJson && importer.mutation === 'installSome') {
            if (!importer.pkg) {
                throw new Error('Cannot save because no package.json found');
            }
            const specsToUsert = resolvedImporter.directDependencies // tslint:disable-line
                .filter((dep) => importer.newPkgRawSpecs.indexOf(dep.specRaw) !== -1)
                .map((dep) => {
                return {
                    name: dep.alias,
                    pref: dep.normalizedPref || getPref_1.default(dep.alias, dep.name, dep.version, {
                        pinnedVersion: importer.pinnedVersion,
                        rawSpec: dep.specRaw,
                    }),
                    saveType: importer.targetDependenciesField,
                };
            });
            for (const pkgToInstall of importer.wantedDeps) {
                if (pkgToInstall.alias && !specsToUsert.some((spec) => spec.name === pkgToInstall.alias)) { // tslint:disable-line
                    specsToUsert.push({
                        name: pkgToInstall.alias,
                        saveType: importer.targetDependenciesField,
                    });
                }
            }
            newPkg = await save_1.default(importer.prefix, specsToUsert);
        }
        else {
            core_loggers_1.packageJsonLogger.debug({
                prefix: importer.prefix,
                updated: importer.pkg,
            });
        }
        if (newPkg) {
            const lockfileImporter = ctx.wantedLockfile.importers[importer.id];
            ctx.wantedLockfile.importers[importer.id] = addDirectDependenciesToLockfile(newPkg, lockfileImporter, importer.linkedPackages, resolvedImporter.directDependencies, ctx.registries);
        }
        const topParents = importer.pkg
            ? await getTopParents(R.difference(R.keys(utils_1.getAllDependenciesFromPackage(importer.pkg)), importer.newPkgRawSpecs && resolvedImporter.directDependencies
                .filter((directDep) => importer.newPkgRawSpecs.indexOf(directDep.specRaw) !== -1)
                .map((directDep) => directDep.alias) || []), importer.modulesDir)
            : [];
        return {
            bin: importer.bin,
            directNodeIdsByAlias: resolvedImporter.directNodeIdsByAlias,
            hoistedAliases: importer.hoistedAliases,
            id: importer.id,
            linkedDependencies: resolvedImporter.linkedDependencies,
            modulesDir: importer.modulesDir,
            pkg: newPkg || importer.pkg,
            prefix: importer.prefix,
            pruneDirectDependencies: importer.pruneDirectDependencies,
            removePackages: importer.removePackages,
            shamefullyFlatten: importer.shamefullyFlatten,
            topParents,
            usesExternalLockfile: importer.usesExternalLockfile,
        };
    }));
    const result = await link_1.default(importersToLink, dependenciesTree, {
        afterAllResolvedHook: opts.hooks && opts.hooks.afterAllResolved,
        currentLockfile: ctx.currentLockfile,
        dryRun: opts.lockfileOnly,
        force: opts.force,
        include: opts.include,
        independentLeaves: opts.independentLeaves,
        lockfileDirectory: opts.lockfileDirectory,
        makePartialCurrentLockfile: opts.makePartialCurrentLockfile,
        outdatedDependencies,
        pruneStore: opts.pruneStore,
        registries: ctx.registries,
        skipped: ctx.skipped,
        storeController: opts.storeController,
        strictPeerDependencies: opts.strictPeerDependencies,
        updateLockfileMinorVersion: opts.updateLockfileMinorVersion,
        virtualStoreDir: ctx.virtualStoreDir,
        wantedLockfile: ctx.wantedLockfile,
        wantedToBeSkippedPackageIds,
    });
    ctx.pendingBuilds = ctx.pendingBuilds
        .filter((relDepPath) => !result.removedDepPaths.has(dp.resolve(ctx.registries, relDepPath)));
    if (opts.ignoreScripts) {
        // we can use concat here because we always only append new packages, which are guaranteed to not be there by definition
        ctx.pendingBuilds = ctx.pendingBuilds
            .concat(result.newDepPaths
            .filter((depPath) => result.depGraph[depPath].requiresBuild)
            .map((depPath) => dp.relative(ctx.registries, result.depGraph[depPath].name, depPath)));
    }
    const lockfileOpts = { forceSharedFormat: opts.forceSharedLockfile };
    if (opts.lockfileOnly) {
        await lockfile_file_1.writeWantedLockfile(ctx.lockfileDirectory, result.wantedLockfile, lockfileOpts);
    }
    else {
        await Promise.all([
            opts.useLockfile
                ? lockfile_file_1.writeLockfiles(ctx.lockfileDirectory, result.wantedLockfile, result.currentLockfile, lockfileOpts)
                : lockfile_file_1.writeCurrentLockfile(ctx.lockfileDirectory, result.currentLockfile, lockfileOpts),
            (() => {
                if (result.currentLockfile.packages === undefined && result.removedDepPaths.size === 0) {
                    return Promise.resolve();
                }
                return modules_yaml_1.write(ctx.virtualStoreDir, Object.assign({}, ctx.modulesFile, { importers: Object.assign({}, ctx.modulesFile && ctx.modulesFile.importers, importersToLink.reduce((acc, importer) => {
                        acc[importer.id] = {
                            hoistedAliases: importer.hoistedAliases,
                            shamefullyFlatten: importer.shamefullyFlatten,
                        };
                        return acc;
                    }, {})), included: ctx.include, independentLeaves: opts.independentLeaves, layoutVersion: constants_1.LAYOUT_VERSION, packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`, pendingBuilds: ctx.pendingBuilds, registries: ctx.registries, skipped: Array.from(ctx.skipped), store: ctx.storePath }));
            })(),
        ]);
        // postinstall hooks
        if (!(opts.ignoreScripts || !result.newDepPaths || !result.newDepPaths.length)) {
            const depPaths = Object.keys(result.depGraph);
            const rootNodes = depPaths.filter((depPath) => result.depGraph[depPath].depth === 0);
            const nodesToBuild = new Set();
            getSubgraphToBuild(result.depGraph, rootNodes, nodesToBuild, new Set());
            const onlyFromBuildGraph = R.filter((depPath) => nodesToBuild.has(depPath));
            const nodesToBuildArray = Array.from(nodesToBuild);
            const graph = new Map(nodesToBuildArray
                .map((depPath) => [depPath, onlyFromBuildGraph(R.values(result.depGraph[depPath].children))]));
            const graphSequencerResult = graphSequencer({
                graph,
                groups: [nodesToBuildArray],
            });
            const chunks = graphSequencerResult.chunks;
            const groups = chunks.map((chunk) => chunk
                .filter((depPath) => result.depGraph[depPath].requiresBuild && !result.depGraph[depPath].isBuilt && result.newDepPaths.indexOf(depPath) !== -1)
                .map((depPath) => result.depGraph[depPath])
                .map((pkg) => async () => {
                try {
                    const hasSideEffects = await lifecycle_1.runPostinstallHooks({
                        depPath: pkg.absolutePath,
                        optional: pkg.optional,
                        pkgRoot: pkg.peripheralLocation,
                        prepare: pkg.prepare,
                        rawNpmConfig: opts.rawNpmConfig,
                        rootNodeModulesDir: ctx.virtualStoreDir,
                        unsafePerm: opts.unsafePerm || false,
                    });
                    if (hasSideEffects && opts.sideEffectsCacheWrite) {
                        try {
                            await opts.storeController.upload(pkg.peripheralLocation, {
                                engine: constants_1.ENGINE_NAME,
                                packageId: pkg.id,
                            });
                        }
                        catch (err) {
                            if (err && err.statusCode === 403) {
                                logger_1.default.warn({
                                    message: `The store server disabled upload requests, could not upload ${pkg.id}`,
                                    prefix: ctx.lockfileDirectory,
                                });
                            }
                            else {
                                logger_1.default.warn({
                                    error: err,
                                    message: `An error occurred while uploading ${pkg.id}`,
                                    prefix: ctx.lockfileDirectory,
                                });
                            }
                        }
                    }
                }
                catch (err) {
                    if (resolvedPackagesByPackageId[pkg.id].optional) {
                        // TODO: add parents field to the log
                        core_loggers_1.skippedOptionalDependencyLogger.debug({
                            details: err.toString(),
                            package: {
                                id: pkg.id,
                                name: pkg.name,
                                version: pkg.version,
                            },
                            prefix: opts.lockfileDirectory,
                            reason: 'build_failure',
                        });
                        return;
                    }
                    throw err;
                }
            }));
            await run_groups_1.default(opts.childConcurrency, groups);
        }
    }
    // waiting till the skipped packages are downloaded to the store
    await Promise.all(R.props(Array.from(ctx.skipped), resolvedPackagesByPackageId)
        // skipped packages might have not been reanalized on a repeat install
        // so lets just ignore those by excluding nulls
        .filter(Boolean)
        .map((pkg) => pkg.fetchingFiles));
    // waiting till package requests are finished
    await Promise.all(R.values(resolvedPackagesByPackageId).map((installed) => installed.finishing));
    core_loggers_1.summaryLogger.debug({ prefix: opts.lockfileDirectory });
    await opts.storeController.close();
}
function modulesIsUpToDate(opts) {
    const currentWithSkipped = [
        ...R.keys(opts.currentLockfile.packages),
        ...opts.skippedRelDepPaths,
    ];
    currentWithSkipped.sort();
    return R.equals(R.keys(opts.wantedLockfile.packages), currentWithSkipped);
}
function getSubgraphToBuild(graph, entryNodes, nodesToBuild, walked) {
    let currentShouldBeBuilt = false;
    for (const depPath of entryNodes) {
        if (nodesToBuild.has(depPath)) {
            currentShouldBeBuilt = true;
        }
        if (walked.has(depPath))
            continue;
        walked.add(depPath);
        const childShouldBeBuilt = getSubgraphToBuild(graph, R.values(graph[depPath].children), nodesToBuild, walked)
            || graph[depPath].requiresBuild;
        if (childShouldBeBuilt) {
            nodesToBuild.add(depPath);
            currentShouldBeBuilt = true;
        }
    }
    return currentShouldBeBuilt;
}
function addDirectDependenciesToLockfile(newPkg, lockfileImporter, linkedPackages, directDependencies, registries) {
    const newLockfileImporter = {
        dependencies: {},
        devDependencies: {},
        optionalDependencies: {},
        specifiers: {},
    };
    linkedPackages.forEach((linkedPkg) => {
        newLockfileImporter.specifiers[linkedPkg.alias] = getSpecFromPackageJson_1.default(newPkg, linkedPkg.alias);
    });
    const directDependenciesByAlias = directDependencies.reduce((acc, directDependency) => {
        acc[directDependency.alias] = directDependency;
        return acc;
    }, {});
    const optionalDependencies = R.keys(newPkg.optionalDependencies);
    const dependencies = R.difference(R.keys(newPkg.dependencies), optionalDependencies);
    const devDependencies = R.difference(R.difference(R.keys(newPkg.devDependencies), optionalDependencies), dependencies);
    const allDeps = R.reduce(R.union, [], [optionalDependencies, devDependencies, dependencies]);
    for (const alias of allDeps) {
        if (directDependenciesByAlias[alias]) {
            const dep = directDependenciesByAlias[alias];
            const ref = lockfile_1.absolutePathToRef(dep.id, {
                alias: dep.alias,
                realName: dep.name,
                registries,
                resolution: dep.resolution,
            });
            if (dep.dev) {
                newLockfileImporter.devDependencies[dep.alias] = ref;
            }
            else if (dep.optional) {
                newLockfileImporter.optionalDependencies[dep.alias] = ref;
            }
            else {
                newLockfileImporter.dependencies[dep.alias] = ref;
            }
            newLockfileImporter.specifiers[dep.alias] = getSpecFromPackageJson_1.default(newPkg, dep.alias);
        }
        else if (lockfileImporter.specifiers[alias]) {
            newLockfileImporter.specifiers[alias] = lockfileImporter.specifiers[alias];
            if (lockfileImporter.dependencies && lockfileImporter.dependencies[alias]) {
                newLockfileImporter.dependencies[alias] = lockfileImporter.dependencies[alias];
            }
            else if (lockfileImporter.optionalDependencies && lockfileImporter.optionalDependencies[alias]) {
                newLockfileImporter.optionalDependencies[alias] = lockfileImporter.optionalDependencies[alias];
            }
            else if (lockfileImporter.devDependencies && lockfileImporter.devDependencies[alias]) {
                newLockfileImporter.devDependencies[alias] = lockfileImporter.devDependencies[alias];
            }
        }
    }
    alignDependencyTypes(newPkg, newLockfileImporter);
    return newLockfileImporter;
}
function alignDependencyTypes(pkg, lockfileImporter) {
    const depTypesOfAliases = getAliasToDependencyTypeMap(pkg);
    // Aligning the dependency types in pnpm-lock.yaml
    for (const depType of types_1.DEPENDENCIES_FIELDS) {
        if (!lockfileImporter[depType])
            continue;
        for (const alias of Object.keys(lockfileImporter[depType] || {})) {
            if (depType === depTypesOfAliases[alias] || !depTypesOfAliases[alias])
                continue;
            lockfileImporter[depTypesOfAliases[alias]][alias] = lockfileImporter[depType][alias];
            delete lockfileImporter[depType][alias];
        }
    }
}
function getAliasToDependencyTypeMap(pkg) {
    const depTypesOfAliases = {};
    for (const depType of types_1.DEPENDENCIES_FIELDS) {
        if (!pkg[depType])
            continue;
        for (const alias of Object.keys(pkg[depType] || {})) {
            if (!depTypesOfAliases[alias]) {
                depTypesOfAliases[alias] = depType;
            }
        }
    }
    return depTypesOfAliases;
}
async function getTopParents(pkgNames, modules) {
    const pkgs = await Promise.all(pkgNames.map((pkgName) => path.join(modules, pkgName)).map(utils_1.safeReadPackageFromDir));
    return pkgs.filter(Boolean).map((pkg) => ({
        name: pkg.name,
        version: pkg.version,
    }));
}
//# sourceMappingURL=index.js.map