"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const link_bins_1 = require("@pnpm/link-bins");
const read_package_json_1 = require("@pnpm/read-package-json");
const utils_1 = require("@pnpm/utils");
const dp = require("dependency-path");
const pLimit = require("p-limit");
const path = require("path");
const R = require("ramda");
const symlinkDir = require("symlink-dir");
const constants_1 = require("../constants");
const resolvePeers_1 = require("./resolvePeers");
const updateShrinkwrap_1 = require("./updateShrinkwrap");
function linkPackages(rootNodeIdsByAlias, pkgGraph, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        // TODO: decide what kind of logging should be here.
        // The `Creating dependency graph` is not good to report in all cases as
        // sometimes node_modules is alread up-to-date
        // logger.info(`Creating dependency graph`)
        const resolvePeersResult = yield resolvePeers_1.default(pkgGraph, rootNodeIdsByAlias, opts.topParents, opts.independentLeaves, opts.baseNodeModules);
        const depGraph = resolvePeersResult.depGraph;
        let { newShrinkwrap, pendingRequiresBuilds } = updateShrinkwrap_1.default(depGraph, opts.wantedShrinkwrap, opts.pkg); // tslint:disable-line:prefer-const
        if (opts.afterAllResolvedHook) {
            newShrinkwrap = opts.afterAllResolvedHook(newShrinkwrap);
        }
        const removedDepPaths = yield utils_1.removeOrphanPackages({
            bin: opts.bin,
            dryRun: opts.dryRun,
            hoistedAliases: opts.hoistedAliases,
            newShrinkwrap,
            oldShrinkwrap: opts.currentShrinkwrap,
            prefix: opts.root,
            shamefullyFlatten: opts.shamefullyFlatten,
            storeController: opts.storeController,
        });
        let depNodes = R.values(depGraph).filter((depNode) => !opts.skipped.has(depNode.id));
        if (!opts.production) {
            depNodes = depNodes.filter((depNode) => depNode.dev !== false || depNode.optional);
        }
        if (!opts.development) {
            depNodes = depNodes.filter((depNode) => depNode.dev !== true);
        }
        if (!opts.optional) {
            depNodes = depNodes.filter((depNode) => !depNode.optional);
        }
        const filterOpts = {
            noDev: !opts.development,
            noOptional: !opts.optional,
            noProd: !opts.production,
            skipped: opts.skipped,
        };
        const newCurrentShrinkwrap = filterShrinkwrap(newShrinkwrap, filterOpts);
        utils_1.stageLogger.debug('importing_started');
        const newDepPaths = yield linkNewPackages(filterShrinkwrap(opts.currentShrinkwrap, filterOpts), newCurrentShrinkwrap, depGraph, opts);
        utils_1.stageLogger.debug('importing_done');
        const rootDepsByDepPath = depNodes
            .filter((depNode) => depNode.depth === 0)
            .reduce((acc, depNode) => {
            acc[depNode.absolutePath] = depNode;
            return acc;
        }, {});
        for (const rootAlias of R.keys(resolvePeersResult.rootAbsolutePathsByAlias)) {
            const pkg = rootDepsByDepPath[resolvePeersResult.rootAbsolutePathsByAlias[rootAlias]];
            if (!pkg)
                continue;
            if (opts.dryRun || !(yield symlinkDependencyTo(rootAlias, pkg, opts.baseNodeModules)).reused) {
                const isDev = opts.pkg.devDependencies && opts.pkg.devDependencies[pkg.name];
                const isOptional = opts.pkg.optionalDependencies && opts.pkg.optionalDependencies[pkg.name];
                utils_1.rootLogger.info({
                    added: {
                        dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
                        id: pkg.id,
                        latest: opts.outdatedPkgs[pkg.id],
                        name: rootAlias,
                        realName: pkg.name,
                        version: pkg.version,
                    },
                });
            }
        }
        if (!opts.dryRun) {
            yield link_bins_1.default(opts.baseNodeModules, opts.bin);
        }
        if (opts.updateShrinkwrapMinorVersion) {
            // Setting `shrinkwrapMinorVersion` is a temporary solution to
            // have new backward-compatible versions of `shrinkwrap.yaml`
            // w/o changing `shrinkwrapVersion`. From version 4, the
            // `shrinkwrapVersion` field allows numbers like 4.1
            newShrinkwrap.shrinkwrapMinorVersion = constants_1.SHRINKWRAP_MINOR_VERSION;
        }
        yield Promise.all(pendingRequiresBuilds.map((pendingRequiresBuild) => __awaiter(this, void 0, void 0, function* () {
            const depNode = depGraph[pendingRequiresBuild.absoluteDepPath];
            if (!depNode.fetchingRawManifest) {
                // This should never ever happen
                throw new Error(`Cannot create shrinkwrap.yaml because raw manifest (aka package.json) wasn't fetched for "${pendingRequiresBuild.absoluteDepPath}"`);
            }
            const filesResponse = yield depNode.fetchingFiles;
            // The npm team suggests to always read the package.json for deciding whether the package has lifecycle scripts
            const pkgJson = yield depNode.fetchingRawManifest;
            depNode.requiresBuild = Boolean(pkgJson.scripts && (pkgJson.scripts.preinstall || pkgJson.scripts.install || pkgJson.scripts.postinstall) ||
                filesResponse.filenames.indexOf('binding.gyp') !== -1 ||
                filesResponse.filenames.some((filename) => !!filename.match(/^[.]hooks[\\/]/)));
            if (depNode.requiresBuild) {
                newShrinkwrap.packages[pendingRequiresBuild.relativeDepPath].requiresBuild = true;
            }
        })));
        let currentShrinkwrap;
        if (opts.makePartialCurrentShrinkwrap) {
            const packages = opts.currentShrinkwrap.packages || {};
            if (newShrinkwrap.packages) {
                for (const relDepPath in newShrinkwrap.packages) { // tslint:disable-line:forin
                    const depPath = dp.resolve(newShrinkwrap.registry, relDepPath);
                    if (depGraph[depPath]) {
                        packages[relDepPath] = newShrinkwrap.packages[relDepPath];
                    }
                }
            }
            currentShrinkwrap = Object.assign({}, newShrinkwrap, { packages });
        }
        else if (opts.production && opts.development && opts.optional) {
            currentShrinkwrap = newShrinkwrap;
        }
        else {
            currentShrinkwrap = newCurrentShrinkwrap;
        }
        // Important: shamefullyFlattenGraph changes depGraph, so keep this at the end
        if (opts.shamefullyFlatten && (opts.reinstallForFlatten || newDepPaths.length > 0 || removedDepPaths.size > 0)) {
            opts.hoistedAliases = yield shamefullyFlattenGraph(depNodes, currentShrinkwrap, opts);
        }
        return {
            currentShrinkwrap,
            depGraph,
            hoistedAliases: opts.hoistedAliases,
            newDepPaths,
            removedDepPaths,
            wantedShrinkwrap: newShrinkwrap,
        };
    });
}
exports.default = linkPackages;
function shamefullyFlattenGraph(depNodes, currentShrinkwrap, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const dependencyPathByAlias = {};
        const aliasesByDependencyPath = {};
        yield Promise.all(depNodes
            // sort by depth and then alphabetically
            .sort((a, b) => {
            const depthDiff = a.depth - b.depth;
            return depthDiff === 0 ? a.name.localeCompare(b.name) : depthDiff;
        })
            // build the alias map and the id map
            .map((depNode) => {
            for (const childAlias of R.keys(depNode.children)) {
                // if this alias is in the root dependencies, skip it
                if (currentShrinkwrap.specifiers[childAlias]) {
                    continue;
                }
                // if this alias has already been taken, skip it
                if (dependencyPathByAlias[childAlias]) {
                    continue;
                }
                const childPath = depNode.children[childAlias];
                dependencyPathByAlias[childAlias] = childPath;
                if (!aliasesByDependencyPath[childPath]) {
                    aliasesByDependencyPath[childPath] = [];
                }
                aliasesByDependencyPath[childPath].push(childAlias);
            }
            return depNode;
        })
            .map((depNode) => __awaiter(this, void 0, void 0, function* () {
            const pkgAliases = aliasesByDependencyPath[depNode.absolutePath];
            if (!pkgAliases) {
                return;
            }
            // TODO when putting logs back in for hoisted packages, you've to put back the condition inside the map,
            // TODO look how it is done in linkPackages
            if (!opts.dryRun) {
                yield Promise.all(pkgAliases.map((pkgAlias) => __awaiter(this, void 0, void 0, function* () {
                    yield symlinkDependencyTo(pkgAlias, depNode, opts.baseNodeModules);
                })));
            }
        })));
        return aliasesByDependencyPath;
    });
}
function filterShrinkwrap(shr, opts) {
    let pairs = R.toPairs(shr.packages || {})
        .filter((pair) => !opts.skipped.has(pair[1].id || dp.resolve(shr.registry, pair[0])));
    if (opts.noProd) {
        pairs = pairs.filter((pair) => pair[1].dev !== false || pair[1].optional);
    }
    if (opts.noDev) {
        pairs = pairs.filter((pair) => pair[1].dev !== true);
    }
    if (opts.noOptional) {
        pairs = pairs.filter((pair) => !pair[1].optional);
    }
    return {
        dependencies: opts.noProd ? {} : shr.dependencies || {},
        devDependencies: opts.noDev ? {} : shr.devDependencies || {},
        optionalDependencies: opts.noOptional ? {} : shr.optionalDependencies || {},
        packages: R.fromPairs(pairs),
        registry: shr.registry,
        shrinkwrapVersion: shr.shrinkwrapVersion,
        specifiers: shr.specifiers,
    };
}
function linkNewPackages(currentShrinkwrap, wantedShrinkwrap, depGraph, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const wantedRelDepPaths = R.keys(wantedShrinkwrap.packages);
        const prevRelDepPaths = R.keys(currentShrinkwrap.packages);
        // TODO: what if the registries differ?
        const newDepPathsSet = new Set((opts.force
            ? wantedRelDepPaths
            : R.difference(wantedRelDepPaths, prevRelDepPaths))
            .map((relDepPath) => dp.resolve(wantedShrinkwrap.registry, relDepPath))
            // when installing a new package, not all the nodes are analyzed
            // just skip the ones that are in the lockfile but were not analyzed
            .filter((depPath) => depGraph[depPath]));
        utils_1.statsLogger.debug({
            added: newDepPathsSet.size,
            prefix: opts.root,
        });
        const existingWithUpdatedDeps = [];
        if (!opts.force && currentShrinkwrap.packages && wantedShrinkwrap.packages) {
            // add subdependencies that have been updated
            // TODO: no need to relink everything. Can be relinked only what was changed
            for (const relDepPath of wantedRelDepPaths) {
                if (currentShrinkwrap.packages[relDepPath] &&
                    (!R.equals(currentShrinkwrap.packages[relDepPath].dependencies, wantedShrinkwrap.packages[relDepPath].dependencies) ||
                        !R.equals(currentShrinkwrap.packages[relDepPath].optionalDependencies, wantedShrinkwrap.packages[relDepPath].optionalDependencies))) {
                    const depPath = dp.resolve(wantedShrinkwrap.registry, relDepPath);
                    // TODO: come up with a test that triggers the usecase of depGraph[depPath] undefined
                    // see related issue: https://github.com/pnpm/pnpm/issues/870
                    if (depGraph[depPath] && !newDepPathsSet.has(depPath)) {
                        existingWithUpdatedDeps.push(depGraph[depPath]);
                    }
                }
            }
        }
        if (!newDepPathsSet.size && !existingWithUpdatedDeps.length)
            return [];
        const newDepPaths = Array.from(newDepPathsSet);
        if (opts.dryRun)
            return newDepPaths;
        const newPkgs = R.props(newDepPaths, depGraph);
        yield Promise.all([
            linkAllModules(newPkgs, depGraph, { optional: opts.optional }),
            linkAllModules(existingWithUpdatedDeps, depGraph, { optional: opts.optional }),
            linkAllPkgs(opts.storeController, newPkgs, opts),
        ]);
        yield linkAllBins(newPkgs, depGraph, { optional: opts.optional });
        return newDepPaths;
    });
}
const limitLinking = pLimit(16);
function linkAllPkgs(storeController, depNodes, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(depNodes.map((depNode) => __awaiter(this, void 0, void 0, function* () {
            const filesResponse = yield depNode.fetchingFiles;
            if (depNode.independent)
                return;
            return storeController.importPackage(depNode.centralLocation, depNode.peripheralLocation, {
                filesResponse,
                force: opts.force,
            });
        })));
    });
}
function linkAllBins(depNodes, depGraph, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(depNodes.map((depNode) => limitLinking(() => __awaiter(this, void 0, void 0, function* () {
            const childrenToLink = opts.optional
                ? depNode.children
                : R.keys(depNode.children)
                    .reduce((nonOptionalChildren, childAlias) => {
                    if (!depNode.optionalDependencies.has(childAlias)) {
                        nonOptionalChildren[childAlias] = depNode.children[childAlias];
                    }
                    return nonOptionalChildren;
                }, {});
            const pkgs = yield Promise.all(R.keys(childrenToLink)
                .filter((alias) => depGraph[childrenToLink[alias]].hasBin && depGraph[childrenToLink[alias]].installable)
                .map((alias) => __awaiter(this, void 0, void 0, function* () {
                const dep = depGraph[childrenToLink[alias]];
                return {
                    location: dep.peripheralLocation,
                    manifest: (yield dep.fetchingRawManifest) || (yield read_package_json_1.fromDir(dep.peripheralLocation)),
                };
            })));
            const binPath = path.join(depNode.peripheralLocation, 'node_modules', '.bin');
            yield link_bins_1.linkBinsOfPackages(pkgs, binPath);
            // link also the bundled dependencies` bins
            if (depNode.hasBundledDependencies) {
                const bundledModules = path.join(depNode.peripheralLocation, 'node_modules');
                yield link_bins_1.default(bundledModules, binPath);
            }
        }))));
    });
}
function linkAllModules(depNodes, depGraph, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(depNodes
            .filter((depNode) => !depNode.independent)
            .map((depNode) => limitLinking(() => __awaiter(this, void 0, void 0, function* () {
            const childrenToLink = opts.optional
                ? depNode.children
                : R.keys(depNode.children)
                    .reduce((nonOptionalChildren, childAlias) => {
                    if (!depNode.optionalDependencies.has(childAlias)) {
                        nonOptionalChildren[childAlias] = depNode.children[childAlias];
                    }
                    return nonOptionalChildren;
                }, {});
            yield Promise.all(R.keys(childrenToLink)
                .map((alias) => __awaiter(this, void 0, void 0, function* () {
                const pkg = depGraph[childrenToLink[alias]];
                if (!pkg.installable)
                    return;
                yield symlinkDependencyTo(alias, pkg, depNode.modules);
            })));
        }))));
    });
}
function symlinkDependencyTo(alias, depNode, dest) {
    dest = path.join(dest, alias);
    return symlinkDir(depNode.peripheralLocation, dest);
}
//# sourceMappingURL=index.js.map