"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 fs = require("mz/fs");
const path = require("path");
const linkDir = require("link-dir");
const symlinkDir = require("symlink-dir");
const exists = require("path-exists");
const pnpm_logger_1 = require("pnpm-logger");
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 shrinkwrap_1 = require("../fs/shrinkwrap");
const removeOrphanPkgs_1 = require("../api/removeOrphanPkgs");
const ncpCB = require("ncp");
const thenify = require("thenify");
const ncp = thenify(ncpCB);
function default_1(topPkgs, rootNodeIds, tree, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const topPkgIds = topPkgs.map(pkg => pkg.id);
        const pkgsToLink = yield resolvePeers_1.default(tree, rootNodeIds, topPkgIds, opts.topParents);
        const newShr = updateShrinkwrap_1.default(pkgsToLink, opts.shrinkwrap, opts.pkg);
        yield removeOrphanPkgs_1.default(opts.privateShrinkwrap, newShr, opts.root, opts.storePath);
        let flatResolvedDeps = R.values(pkgsToLink).filter(dep => !opts.skipped.has(dep.id));
        if (opts.production) {
            flatResolvedDeps = flatResolvedDeps.filter(dep => !dep.dev);
        }
        if (!opts.optional) {
            flatResolvedDeps = flatResolvedDeps.filter(dep => !dep.optional);
        }
        const filterOpts = {
            noDev: opts.production,
            noOptional: !opts.optional,
            skipped: opts.skipped,
        };
        const newPkgResolvedIds = yield linkNewPackages(filterShrinkwrap(opts.privateShrinkwrap, filterOpts), filterShrinkwrap(newShr, filterOpts), pkgsToLink, opts);
        for (let pkg of flatResolvedDeps.filter(pkg => pkg.depth === 0)) {
            yield symlinkDependencyTo(pkg, opts.baseNodeModules);
            logInstallStatus_1.default({
                status: 'installed',
                pkgId: pkg.id,
            });
        }
        yield linkBins_1.default(opts.baseNodeModules, opts.bin);
        return {
            linkedPkgsMap: pkgsToLink,
            shrinkwrap: newShr,
            newPkgResolvedIds,
        };
    });
}
exports.default = default_1;
function filterShrinkwrap(shr, opts) {
    let pairs = R.toPairs(shr.packages)
        .filter(pair => !opts.skipped.has(pair[1].id || shrinkwrap_1.shortIdToFullId(pair[0], shr.registry)));
    if (opts.noDev) {
        pairs = pairs.filter(pair => !pair[1].dev);
    }
    if (opts.noOptional) {
        pairs = pairs.filter(pair => !pair[1].optional);
    }
    return {
        version: shr.version,
        registry: shr.registry,
        specifiers: shr.specifiers,
        packages: R.fromPairs(pairs),
    };
}
function linkNewPackages(privateShrinkwrap, shrinkwrap, pkgsToLink, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const nextPkgResolvedIds = R.keys(shrinkwrap.packages);
        const prevPkgResolvedIds = R.keys(privateShrinkwrap.packages);
        // TODO: what if the registries differ?
        const newPkgResolvedIds = (opts.force
            ? nextPkgResolvedIds
            : R.difference(nextPkgResolvedIds, prevPkgResolvedIds))
            .map(shortId => shrinkwrap_1.shortIdToFullId(shortId, shrinkwrap.registry));
        const newPkgs = R.props(newPkgResolvedIds, pkgsToLink);
        try {
            yield linkAllPkgs(linkPkg, newPkgs, opts);
        }
        catch (err) {
            if (!err.message.startsWith('EXDEV: cross-device link not permitted'))
                throw err;
            pnpm_logger_1.default.warn(err.message);
            pnpm_logger_1.default.info('Falling back to copying packages from store');
            yield linkAllPkgs(copyPkg, newPkgs, opts);
        }
        if (!opts.force) {
            // add subdependencies that have been updated
            // TODO: no need to relink everything. Can be relinked only what was changed
            for (const shortId of nextPkgResolvedIds) {
                if (privateShrinkwrap.packages[shortId] &&
                    !R.equals(privateShrinkwrap.packages[shortId].dependencies, shrinkwrap.packages[shortId].dependencies)) {
                    const resolvedId = shrinkwrap_1.shortIdToFullId(shortId, shrinkwrap.registry);
                    newPkgs.push(pkgsToLink[resolvedId]);
                }
            }
        }
        yield linkAllModules(newPkgs, pkgsToLink, { optional: opts.optional });
        return newPkgResolvedIds;
    });
}
const limitLinking = pLimit(16);
function linkAllPkgs(linkPkg, alldeps, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(alldeps.map(pkg => limitLinking(() => linkPkg(pkg, opts))));
    });
}
function linkAllModules(pkgs, pkgMap, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(pkgs.map(pkg => limitLinking(() => linkModules(pkg, pkgMap, opts))));
    });
}
function linkPkg(dependency, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const newlyFetched = yield dependency.fetchingFiles;
        const pkgJsonPath = path.join(dependency.hardlinkedLocation, 'package.json');
        if (newlyFetched || opts.force || !(yield exists(pkgJsonPath)) || !(yield pkgLinkedToStore(pkgJsonPath, dependency))) {
            yield linkDir(dependency.path, dependency.hardlinkedLocation);
        }
    });
}
function copyPkg(dependency, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const newlyFetched = yield dependency.fetchingFiles;
        const pkgJsonPath = path.join(dependency.hardlinkedLocation, 'package.json');
        if (newlyFetched || opts.force || !(yield exists(pkgJsonPath))) {
            yield ncp(dependency.path, dependency.hardlinkedLocation);
        }
    });
}
function pkgLinkedToStore(pkgJsonPath, dependency) {
    return __awaiter(this, void 0, void 0, function* () {
        const pkgJsonPathInStore = path.join(dependency.path, 'package.json');
        if (yield isSameFile(pkgJsonPath, pkgJsonPathInStore))
            return true;
        pnpm_logger_1.default.info(`Relinking ${dependency.hardlinkedLocation} from the store`);
        return false;
    });
}
function isSameFile(file1, file2) {
    return __awaiter(this, void 0, void 0, function* () {
        const stats = yield Promise.all([fs.stat(file1), fs.stat(file2)]);
        return stats[0].ino === stats[1].ino;
    });
}
function linkModules(dependency, pkgMap, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const childrenToLink = opts.optional
            ? dependency.children
            : dependency.children.filter(child => !dependency.optionalDependencies.has(pkgMap[child].name));
        yield Promise.all(R.props(childrenToLink, pkgMap)
            .filter(child => child.installable)
            .map(child => symlinkDependencyTo(child, dependency.modules)));
        const binPath = path.join(dependency.hardlinkedLocation, 'node_modules', '.bin');
        yield linkBins_1.default(dependency.modules, binPath, dependency.name);
        // link also the bundled dependencies` bins
        if (dependency.hasBundledDependencies) {
            const bundledModules = path.join(dependency.hardlinkedLocation, 'node_modules');
            yield linkBins_1.default(bundledModules, binPath);
        }
    });
}
function symlinkDependencyTo(dependency, dest) {
    dest = path.join(dest, dependency.name);
    return symlinkDir(dependency.hardlinkedLocation, dest);
}
//# sourceMappingURL=index.js.map