"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 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 dp = require("dependency-path");
const removeOrphanPkgs_1 = require("../api/removeOrphanPkgs");
const linkIndexedDir_1 = require("../fs/linkIndexedDir");
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);
        // 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 pkgsToLink = yield resolvePeers_1.default(tree, rootNodeIds, topPkgIds, opts.topParents, opts.independentLeaves, opts.baseNodeModules);
        const newShr = updateShrinkwrap_1.default(pkgsToLink, opts.wantedShrinkwrap, opts.pkg);
        yield removeOrphanPkgs_1.default({
            oldShrinkwrap: opts.currentShrinkwrap,
            newShrinkwrap: newShr,
            prefix: opts.root,
            store: opts.storePath,
            storeIndex: opts.storeIndex,
            bin: opts.bin,
        });
        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);
        const newPkgResolvedIds = yield linkNewPackages(filterShrinkwrap(opts.currentShrinkwrap, filterOpts), newCurrentShrinkwrap, pkgsToLink, opts);
        for (let pkg of flatResolvedDeps.filter(pkg => pkg.depth === 0)) {
            const symlinkingResult = yield symlinkDependencyTo(pkg, opts.baseNodeModules);
            if (!symlinkingResult.reused) {
                const isDev = opts.pkg.devDependencies && opts.pkg.devDependencies[pkg.name];
                const isOptional = opts.pkg.optionalDependencies && opts.pkg.optionalDependencies[pkg.name];
                pnpm_logger_1.rootLogger.info({
                    added: {
                        id: pkg.id,
                        name: pkg.name,
                        version: pkg.version,
                        dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
                    },
                });
            }
            logInstallStatus_1.default({
                status: 'installed',
                pkgId: pkg.id,
            });
        }
        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 = 2;
        }
        let currentShrinkwrap;
        if (opts.makePartialCurrentShrinkwrap) {
            const packages = opts.currentShrinkwrap.packages || {};
            if (newShr.packages) {
                for (const shortId in newShr.packages) {
                    const resolvedId = dp.resolve(newShr.registry, shortId);
                    if (pkgsToLink[resolvedId]) {
                        packages[shortId] = newShr.packages[shortId];
                    }
                }
            }
            currentShrinkwrap = Object.assign({}, newShr, {
                packages,
            });
        }
        else if (opts.production && opts.development && opts.optional) {
            currentShrinkwrap = newShr;
        }
        else {
            currentShrinkwrap = newCurrentShrinkwrap;
        }
        return {
            linkedPkgsMap: pkgsToLink,
            wantedShrinkwrap: newShr,
            currentShrinkwrap,
            newPkgResolvedIds,
        };
    });
}
exports.default = default_1;
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 nextPkgResolvedIds = R.keys(wantedShrinkwrap.packages);
        const prevPkgResolvedIds = R.keys(currentShrinkwrap.packages);
        // TODO: what if the registries differ?
        const newPkgResolvedIds = (opts.force
            ? nextPkgResolvedIds
            : R.difference(nextPkgResolvedIds, prevPkgResolvedIds))
            .map(shortId => dp.resolve(wantedShrinkwrap.registry, shortId))
            .filter(resolvedId => pkgsToLink[resolvedId]);
        const newPkgs = R.props(newPkgResolvedIds, pkgsToLink);
        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 shortId of nextPkgResolvedIds) {
                if (currentShrinkwrap.packages[shortId] &&
                    (!R.equals(currentShrinkwrap.packages[shortId].dependencies, wantedShrinkwrap.packages[shortId].dependencies) ||
                        !R.equals(currentShrinkwrap.packages[shortId].optionalDependencies, wantedShrinkwrap.packages[shortId].optionalDependencies))) {
                    const resolvedId = dp.resolve(wantedShrinkwrap.registry, shortId);
                    // TODO: come up with a test that triggers the usecase of pkgsToLink[resolvedId] undefined
                    // see related issue: https://github.com/pnpm/pnpm/issues/870
                    if (pkgsToLink[resolvedId]) {
                        newPkgs.push(pkgsToLink[resolvedId]);
                    }
                }
            }
        }
        if (!newPkgs.length)
            return [];
        pnpm_logger_1.default.info(`Adding ${newPkgs.length} packages to node_modules`);
        yield Promise.all([
            linkAllModules(newPkgs, pkgsToLink, { optional: opts.optional }),
            (() => __awaiter(this, void 0, void 0, function* () {
                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);
                }
            }))()
        ]);
        yield linkAllBins(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) => __awaiter(this, void 0, void 0, function* () {
            const fetchResult = yield pkg.fetchingFiles;
            if (pkg.independent)
                return;
            return limitLinking(() => linkPkg(fetchResult, pkg, opts));
        })));
    });
}
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.hardlinkedLocation, 'node_modules', '.bin');
            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 => linkBins_1.linkPkgBins(path.join(dependency.modules, child.name), binPath)));
            // link also the bundled dependencies` bins
            if (dependency.hasBundledDependencies) {
                const bundledModules = path.join(dependency.hardlinkedLocation, '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
                : 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)));
        }))));
    });
}
function linkPkg(fetchResult, dependency, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const pkgJsonPath = path.join(dependency.hardlinkedLocation, 'package.json');
        if (fetchResult.isNew || opts.force || !(yield exists(pkgJsonPath)) || !(yield pkgLinkedToStore(pkgJsonPath, dependency))) {
            yield linkIndexedDir_1.default(dependency.path, dependency.hardlinkedLocation, fetchResult.index);
        }
    });
}
function copyPkg(fetchResult, 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 symlinkDependencyTo(dependency, dest) {
    dest = path.join(dest, dependency.name);
    return symlinkDir(dependency.hardlinkedLocation, dest);
}
//# sourceMappingURL=index.js.map