"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 core_loggers_1 = require("@pnpm/core-loggers");
const link_bins_1 = require("@pnpm/link-bins");
const logger_1 = require("@pnpm/logger");
const modules_cleaner_1 = require("@pnpm/modules-cleaner");
const read_package_json_1 = require("@pnpm/read-package-json");
const shamefully_flatten_1 = require("@pnpm/shamefully-flatten");
const symlink_dependency_1 = require("@pnpm/symlink-dependency");
const dp = require("dependency-path");
const pLimit = require("p-limit");
const path = require("path");
const pnpm_shrinkwrap_1 = require("pnpm-shrinkwrap");
const R = require("ramda");
const constants_1 = require("../constants");
const resolvePeers_1 = require("./resolvePeers");
const updateShrinkwrap_1 = require("./updateShrinkwrap");
function linkPackages(importers, dependenciesTree, 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 { depGraph, importersDirectAbsolutePathsByAlias } = resolvePeers_1.default({
            dependenciesTree,
            importers,
            independentLeaves: opts.independentLeaves,
            strictPeerDependencies: opts.strictPeerDependencies,
            virtualStoreDir: opts.virtualStoreDir,
        });
        for (const importer of importers) {
            if (!importer.usesExternalShrinkwrap)
                continue;
            const directAbsolutePathsByAlias = importersDirectAbsolutePathsByAlias[importer.id];
            for (const alias of R.keys(directAbsolutePathsByAlias)) {
                const depPath = directAbsolutePathsByAlias[alias];
                if (depGraph[depPath].isPure)
                    continue;
                const shrImporter = opts.wantedShrinkwrap.importers[importer.id];
                const ref = dp.relative(opts.registries.default, depPath);
                if (shrImporter.dependencies && shrImporter.dependencies[alias]) {
                    shrImporter.dependencies[alias] = ref;
                }
                else if (shrImporter.devDependencies && shrImporter.devDependencies[alias]) {
                    shrImporter.devDependencies[alias] = ref;
                }
                else if (shrImporter.optionalDependencies && shrImporter.optionalDependencies[alias]) {
                    shrImporter.optionalDependencies[alias] = ref;
                }
            }
        }
        let { newShrinkwrap, pendingRequiresBuilds } = updateShrinkwrap_1.default(depGraph, opts.wantedShrinkwrap, opts.virtualStoreDir, opts.registries.default); // tslint:disable-line:prefer-const
        if (opts.afterAllResolvedHook) {
            newShrinkwrap = opts.afterAllResolvedHook(newShrinkwrap);
        }
        let depNodes = R.values(depGraph).filter((depNode) => {
            const relDepPath = dp.relative(opts.registries.default, depNode.absolutePath);
            if (newShrinkwrap.packages && newShrinkwrap.packages[relDepPath] && !newShrinkwrap.packages[relDepPath].optional) {
                opts.skipped.delete(depNode.id);
                return true;
            }
            return !opts.skipped.has(depNode.id);
        });
        if (!opts.include.dependencies) {
            depNodes = depNodes.filter((depNode) => depNode.dev !== false || depNode.optional);
        }
        if (!opts.include.devDependencies) {
            depNodes = depNodes.filter((depNode) => depNode.dev !== true);
        }
        if (!opts.include.optionalDependencies) {
            depNodes = depNodes.filter((depNode) => !depNode.optional);
        }
        const filterOpts = {
            defaultRegistry: opts.registries.default,
            include: opts.include,
            skipped: opts.skipped,
        };
        const removedDepPaths = yield modules_cleaner_1.prune({
            dryRun: opts.dryRun,
            importers,
            newShrinkwrap: pnpm_shrinkwrap_1.filter(newShrinkwrap, filterOpts),
            oldShrinkwrap: opts.currentShrinkwrap,
            pruneStore: opts.pruneStore,
            registries: opts.registries,
            shrinkwrapDirectory: opts.shrinkwrapDirectory,
            storeController: opts.storeController,
            virtualStoreDir: opts.virtualStoreDir,
        });
        core_loggers_1.stageLogger.debug('importing_started');
        const importerIds = importers.map((importer) => importer.id);
        const newCurrentShrinkwrap = pnpm_shrinkwrap_1.filterByImporters(newShrinkwrap, importerIds, Object.assign({}, filterOpts, { failOnMissingDependencies: true }));
        const newDepPaths = yield linkNewPackages(pnpm_shrinkwrap_1.filterByImporters(opts.currentShrinkwrap, importerIds, Object.assign({}, filterOpts, { failOnMissingDependencies: false })), newCurrentShrinkwrap, depGraph, {
            dryRun: opts.dryRun,
            force: opts.force,
            optional: opts.include.optionalDependencies,
            registries: opts.registries,
            shrinkwrapDirectory: opts.shrinkwrapDirectory,
            storeController: opts.storeController,
            virtualStoreDir: opts.virtualStoreDir,
        });
        core_loggers_1.stageLogger.debug('importing_done');
        const rootDepsByDepPath = depNodes
            .filter((depNode) => depNode.depth === 0)
            .reduce((acc, depNode) => {
            acc[depNode.absolutePath] = depNode;
            return acc;
        }, {});
        yield Promise.all(importers.map((importer) => {
            const directAbsolutePathsByAlias = importersDirectAbsolutePathsByAlias[importer.id];
            const { modulesDir, pkg, prefix } = importer;
            return Promise.all(R.keys(directAbsolutePathsByAlias)
                .map((rootAlias) => ({ rootAlias, depGraphNode: rootDepsByDepPath[directAbsolutePathsByAlias[rootAlias]] }))
                .filter(({ depGraphNode }) => depGraphNode)
                .map(({ rootAlias, depGraphNode }) => __awaiter(this, void 0, void 0, function* () {
                if (!opts.dryRun &&
                    (yield symlink_dependency_1.default(depGraphNode.peripheralLocation, modulesDir, rootAlias)).reused)
                    return;
                const isDev = pkg.devDependencies && pkg.devDependencies[depGraphNode.name];
                const isOptional = pkg.optionalDependencies && pkg.optionalDependencies[depGraphNode.name];
                core_loggers_1.rootLogger.debug({
                    added: {
                        dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
                        id: depGraphNode.id,
                        latest: opts.outdatedDependencies[depGraphNode.id],
                        name: rootAlias,
                        realName: depGraphNode.name,
                        version: depGraphNode.version,
                    },
                    prefix,
                });
            })));
        }));
        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.shrinkwrapVersion = Math.floor(newShrinkwrap.shrinkwrapVersion) === Math.floor(constants_1.SHRINKWRAP_VERSION)
                ? constants_1.SHRINKWRAP_VERSION
                : constants_1.SHRINKWRAP_NEXT_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[\\/]/)));
            // TODO: try to cover with unit test the case when entry is no longer available in shrinkwrap
            // It is an edge that probably happens if the entry is removed during shrinkwrap prune
            if (depNode.requiresBuild && newShrinkwrap.packages[pendingRequiresBuild.relativeDepPath]) {
                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(opts.registries.default, relDepPath);
                    if (depGraph[depPath]) {
                        packages[relDepPath] = newShrinkwrap.packages[relDepPath];
                    }
                }
            }
            const importers = importerIds.reduce((acc, importerId) => {
                acc[importerId] = newShrinkwrap.importers[importerId];
                return acc;
            }, {});
            currentShrinkwrap = Object.assign({}, newShrinkwrap, { packages, importers });
        }
        else if (opts.include.dependencies && opts.include.devDependencies && opts.include.optionalDependencies && opts.skipped.size === 0) {
            currentShrinkwrap = newShrinkwrap;
        }
        else {
            currentShrinkwrap = newCurrentShrinkwrap;
        }
        // Important: shamefullyFlattenGraph changes depGraph, so keep this at the end, right before linkBins
        if (newDepPaths.length > 0 || removedDepPaths.size > 0) {
            yield Promise.all(importers.filter((importer) => importer.shamefullyFlatten)
                .map((importer) => __awaiter(this, void 0, void 0, function* () {
                importer.hoistedAliases = yield shamefully_flatten_1.default(depNodes, currentShrinkwrap.importers[importer.id].specifiers, {
                    dryRun: opts.dryRun,
                    modulesDir: importer.modulesDir,
                });
            })));
        }
        if (!opts.dryRun) {
            yield Promise.all(importers.map((importer) => Promise.all(importer.linkedDependencies.map((linkedDependency) => {
                const depLocation = resolvePath(importer.prefix, linkedDependency.resolution.directory);
                return symlink_dependency_1.symlinkDirectRootDependency(depLocation, importer.modulesDir, linkedDependency.alias, {
                    fromDependenciesField: linkedDependency.dev && 'devDependencies' || linkedDependency.optional && 'optionalDependencies' || 'dependencies',
                    linkedPackage: linkedDependency,
                    prefix: importer.prefix,
                });
            }))));
            yield Promise.all(importers.map(linkBinsOfImporter));
        }
        return {
            currentShrinkwrap,
            depGraph,
            newDepPaths,
            removedDepPaths,
            wantedShrinkwrap: newShrinkwrap,
        };
    });
}
exports.default = linkPackages;
function linkBinsOfImporter({ modulesDir, bin, prefix }) {
    const warn = (message) => logger_1.default.warn({ message, prefix });
    return link_bins_1.default(modulesDir, bin, { warn });
}
const isAbsolutePath = /^[/]|^[A-Za-z]:/;
// This function is copied from @pnpm/local-resolver
function resolvePath(where, spec) {
    if (isAbsolutePath.test(spec))
        return spec;
    return path.resolve(where, spec);
}
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(opts.registries.default, 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]));
        core_loggers_1.statsLogger.debug({
            added: newDepPathsSet.size,
            prefix: opts.shrinkwrapDirectory,
        });
        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(opts.registries.default, 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,
            warn: (message) => logger_1.default.warn({ message, prefix: opts.shrinkwrapDirectory }),
        });
        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, { warn: opts.warn });
            // link also the bundled dependencies` bins
            if (depNode.hasBundledDependencies) {
                const bundledModules = path.join(depNode.peripheralLocation, 'node_modules');
                yield link_bins_1.default(bundledModules, binPath, { warn: opts.warn });
            }
        }))));
    });
}
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 && pkg.optional)
                    return;
                yield symlink_dependency_1.default(pkg.peripheralLocation, depNode.modules, alias);
            })));
        }))));
    });
}
//# sourceMappingURL=link.js.map