"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 path = require("path");
const symlinkDir = require("symlink-dir");
const R = require("ramda");
const pLimit = require("p-limit");
const linkBins_1 = require("./linkBins");
const resolvePeers_1 = require("./resolvePeers");
const logInstallStatus_1 = require("../logging/logInstallStatus");
const updateShrinkwrap_1 = require("./updateShrinkwrap");
const dp = require("dependency-path");
const removeOrphanPkgs_1 = require("../api/removeOrphanPkgs");
const loggers_1 = require("../loggers");
function linkPackages(rootNodeIdsByAlias, tree, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        // TODO: decide what kind of logging should be here.
        // The `Creating dependency tree` is not good to report in all cases as
        // sometimes node_modules is alread up-to-date
        // logger.info(`Creating dependency tree`)
        const resolvePeersResult = yield resolvePeers_1.default(tree, rootNodeIdsByAlias, opts.topParents, opts.independentLeaves, opts.baseNodeModules);
        const pkgsToLink = resolvePeersResult.resolvedTree;
        const newShr = updateShrinkwrap_1.default(pkgsToLink, opts.wantedShrinkwrap, opts.pkg);
        const removedPkgIds = yield removeOrphanPkgs_1.default({
            dryRun: opts.dryRun,
            oldShrinkwrap: opts.currentShrinkwrap,
            newShrinkwrap: newShr,
            prefix: opts.root,
            shamefullyFlatten: opts.shamefullyFlatten,
            storeController: opts.storeController,
            bin: opts.bin,
            hoistedAliases: opts.hoistedAliases,
        });
        let flatResolvedDeps = R.values(pkgsToLink).filter(dep => !opts.skipped.has(dep.id));
        if (!opts.production) {
            flatResolvedDeps = flatResolvedDeps.filter(dep => dep.dev !== false || dep.optional);
        }
        if (!opts.development) {
            flatResolvedDeps = flatResolvedDeps.filter(dep => dep.dev !== true);
        }
        if (!opts.optional) {
            flatResolvedDeps = flatResolvedDeps.filter(dep => !dep.optional);
        }
        const filterOpts = {
            noProd: !opts.production,
            noDev: !opts.development,
            noOptional: !opts.optional,
            skipped: opts.skipped,
        };
        const newCurrentShrinkwrap = filterShrinkwrap(newShr, filterOpts);
        loggers_1.stageLogger.debug('importing_started');
        const newDepPaths = yield linkNewPackages(filterShrinkwrap(opts.currentShrinkwrap, filterOpts), newCurrentShrinkwrap, pkgsToLink, opts);
        loggers_1.stageLogger.debug('importing_done');
        const rootPkgsToLinkByAbsolutePath = flatResolvedDeps
            .filter(pkg => pkg.depth === 0)
            .reduce((rootPkgsToLink, pkg) => {
            rootPkgsToLink[pkg.absolutePath] = pkg;
            return rootPkgsToLink;
        }, {});
        for (let rootAlias of R.keys(resolvePeersResult.rootAbsolutePathsByAlias)) {
            const pkg = rootPkgsToLinkByAbsolutePath[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];
                loggers_1.rootLogger.info({
                    added: {
                        id: pkg.id,
                        name: rootAlias,
                        realName: pkg.name,
                        version: pkg.version,
                        latest: opts.outdatedPkgs[pkg.id],
                        dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
                    },
                });
            }
            logInstallStatus_1.default({
                status: 'installed',
                pkgId: pkg.id,
            });
        }
        if (!opts.dryRun) {
            yield linkBins_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
            newShr.shrinkwrapMinorVersion = 4;
        }
        let currentShrinkwrap;
        if (opts.makePartialCurrentShrinkwrap) {
            const packages = opts.currentShrinkwrap.packages || {};
            if (newShr.packages) {
                for (const relDepPath in newShr.packages) {
                    const depPath = dp.resolve(newShr.registry, relDepPath);
                    if (pkgsToLink[depPath]) {
                        packages[relDepPath] = newShr.packages[relDepPath];
                    }
                }
            }
            currentShrinkwrap = Object.assign({}, newShr, { packages });
        }
        else if (opts.production && opts.development && opts.optional) {
            currentShrinkwrap = newShr;
        }
        else {
            currentShrinkwrap = newCurrentShrinkwrap;
        }
        // Important: shamefullyFlattenTree changes flatResolvedDeps, so keep this at the end
        if (opts.shamefullyFlatten && (opts.reinstallForFlatten || newDepPaths.length > 0 || removedPkgIds.size > 0)) {
            opts.hoistedAliases = yield shamefullyFlattenTree(flatResolvedDeps, currentShrinkwrap, opts);
        }
        return {
            linkedPkgsMap: pkgsToLink,
            wantedShrinkwrap: newShr,
            currentShrinkwrap,
            newDepPaths,
            removedPkgIds,
            hoistedAliases: opts.hoistedAliases,
        };
    });
}
exports.default = linkPackages;
function shamefullyFlattenTree(flatResolvedDeps, currentShrinkwrap, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const pkgIdByAlias = {};
        const aliasByPkgId = {};
        yield Promise.all(flatResolvedDeps
            .sort((a, b) => {
            const depthDiff = a.depth - b.depth;
            return depthDiff === 0 ? a.name.localeCompare(b.name) : depthDiff;
        })
            .map(pkg => {
            for (let childAlias of R.keys(pkg.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 (pkgIdByAlias[childAlias]) {
                    continue;
                }
                const childId = pkg.children[childAlias];
                pkgIdByAlias[childAlias] = childId;
                if (!aliasByPkgId[childId]) {
                    aliasByPkgId[childId] = [];
                }
                aliasByPkgId[childId].push(childAlias);
            }
            return pkg;
        })
            .map((pkg) => __awaiter(this, void 0, void 0, function* () {
            const pkgAliases = aliasByPkgId[pkg.id];
            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, pkg, opts.baseNodeModules);
                })));
            }
        })));
        return aliasByPkgId;
    });
}
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 {
        shrinkwrapVersion: shr.shrinkwrapVersion,
        registry: shr.registry,
        specifiers: shr.specifiers,
        packages: R.fromPairs(pairs),
        dependencies: opts.noProd ? {} : shr.dependencies || {},
        devDependencies: opts.noDev ? {} : shr.devDependencies || {},
        optionalDependencies: opts.noOptional ? {} : shr.optionalDependencies || {},
    };
}
function linkNewPackages(currentShrinkwrap, wantedShrinkwrap, pkgsToLink, 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))
            .filter(depPath => pkgsToLink[depPath]));
        loggers_1.statsLogger.debug({ added: newDepPathsSet.size });
        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 pkgsToLink[depPath] undefined
                    // see related issue: https://github.com/pnpm/pnpm/issues/870
                    if (pkgsToLink[depPath] && !newDepPathsSet.has(depPath)) {
                        existingWithUpdatedDeps.push(pkgsToLink[depPath]);
                    }
                }
            }
        }
        if (!newDepPathsSet.size && !existingWithUpdatedDeps.length)
            return [];
        const newDepPaths = Array.from(newDepPathsSet);
        if (opts.dryRun)
            return newDepPaths;
        const newPkgs = R.props(newDepPaths, pkgsToLink);
        yield Promise.all([
            linkAllModules(newPkgs, pkgsToLink, { optional: opts.optional }),
            linkAllModules(existingWithUpdatedDeps, pkgsToLink, { optional: opts.optional }),
            linkAllPkgs(opts.storeController, newPkgs, opts),
        ]);
        yield linkAllBins(newPkgs, pkgsToLink, { optional: opts.optional });
        return newDepPaths;
    });
}
const limitLinking = pLimit(16);
function linkAllPkgs(storeController, alldeps, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(alldeps.map((pkg) => __awaiter(this, void 0, void 0, function* () {
            const filesResponse = yield pkg.fetchingFiles;
            if (pkg.independent)
                return;
            return storeController.importPackage(pkg.centralLocation, pkg.peripheralLocation, {
                force: opts.force,
                filesResponse,
            });
        })));
    });
}
function linkAllBins(pkgs, pkgMap, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(pkgs.map(dependency => limitLinking(() => __awaiter(this, void 0, void 0, function* () {
            const binPath = path.join(dependency.peripheralLocation, 'node_modules', '.bin');
            const childrenToLink = opts.optional
                ? dependency.children
                : R.keys(dependency.children)
                    .reduce((nonOptionalChildren, childAlias) => {
                    if (!dependency.optionalDependencies.has(childAlias)) {
                        nonOptionalChildren[childAlias] = dependency.children[childAlias];
                    }
                    return nonOptionalChildren;
                }, {});
            yield Promise.all(R.keys(childrenToLink)
                .map((alias) => __awaiter(this, void 0, void 0, function* () {
                const childToLink = childrenToLink[alias];
                const child = pkgMap[childToLink];
                if (child.installable) {
                    yield linkBins_1.linkPkgBins(path.join(dependency.modules, alias), binPath);
                }
            })));
            // link also the bundled dependencies` bins
            if (dependency.hasBundledDependencies) {
                const bundledModules = path.join(dependency.peripheralLocation, 'node_modules');
                yield linkBins_1.default(bundledModules, binPath);
            }
        }))));
    });
}
function linkAllModules(pkgs, pkgMap, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(pkgs
            .filter(dependency => !dependency.independent)
            .map(dependency => limitLinking(() => __awaiter(this, void 0, void 0, function* () {
            const childrenToLink = opts.optional
                ? dependency.children
                : R.keys(dependency.children)
                    .reduce((nonOptionalChildren, childAlias) => {
                    if (!dependency.optionalDependencies.has(childAlias)) {
                        nonOptionalChildren[childAlias] = dependency.children[childAlias];
                    }
                    return nonOptionalChildren;
                }, {});
            yield Promise.all(R.keys(childrenToLink)
                .map((alias) => __awaiter(this, void 0, void 0, function* () {
                const pkg = pkgMap[childrenToLink[alias]];
                if (!pkg.installable)
                    return;
                yield symlinkDependencyTo(alias, pkg, dependency.modules);
            })));
        }))));
    });
}
function symlinkDependencyTo(alias, dependency, dest) {
    dest = path.join(dest, alias);
    return symlinkDir(dependency.peripheralLocation, dest);
}
//# sourceMappingURL=index.js.map