"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 npa = require("npm-package-arg");
const pnpm_logger_1 = require("pnpm-logger");
const pFilter = require("p-filter");
const fetch_1 = require("./fetch");
const link_dir_1 = require("link-dir");
const mkdirp = require("mkdirp-promise");
const installChecks = require("pnpm-install-checks");
const pnpmPkgJson_1 = require("../pnpmPkgJson");
const symlink_dir_1 = require("symlink-dir");
const exists = require("exists-file");
const graphController_1 = require("../fs/graphController");
const logInstallStatus_1 = require("../logging/logInstallStatus");
const fs = require("mz/fs");
const linkBins_1 = require("../install/linkBins");
const installCheckLogger = pnpm_logger_1.default('install-check');
function installAll(ctx, dependencies, optionalDependencies, modules, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const keypath = options.keypath || [];
        const nonOptionalDependencies = Object.keys(dependencies)
            .filter(depName => !optionalDependencies[depName])
            .reduce((nonOptionalDependencies, depName) => {
            nonOptionalDependencies[depName] = dependencies[depName];
            return nonOptionalDependencies;
        }, {});
        const installedPkgs = Array.prototype.concat.apply([], yield Promise.all([
            installMultiple(ctx, nonOptionalDependencies, modules, Object.assign({}, options, { optional: false, keypath })),
            installMultiple(ctx, optionalDependencies, modules, Object.assign({}, options, { optional: true, keypath })),
        ]));
        if (options.fetchingFiles) {
            yield options.fetchingFiles;
        }
        yield mkdirp(modules);
        yield Promise.all(installedPkgs
            .map(function (subdep) {
            return __awaiter(this, void 0, void 0, function* () {
                const dest = path.join(modules, subdep.pkg.name);
                yield symlink_dir_1.default(subdep.hardlinkedLocation, dest);
            });
        }));
        return installedPkgs;
    });
}
exports.default = installAll;
function installMultiple(ctx, pkgsMap, modules, options) {
    return __awaiter(this, void 0, void 0, function* () {
        pkgsMap = pkgsMap || {};
        const pkgs = Object.keys(pkgsMap).map(pkgName => getRawSpec(pkgName, pkgsMap[pkgName]));
        ctx.graph = ctx.graph || {};
        const specs = pkgs.map(npa);
        const nonLinkedPkgs = modules === options.baseNodeModules
            ? yield pFilter(specs, (spec) => isInnerLink(modules, spec.name))
            : specs;
        const installedPkgs = (yield Promise.all(nonLinkedPkgs
            .map((spec) => __awaiter(this, void 0, void 0, function* () {
            const pkgId = options.resolvedDependencies &&
                options.resolvedDependencies[spec.name];
            const dependencyShrinkwrap = pkgId && ctx.shrinkwrap.packages[pkgId];
            try {
                const pkg = yield install(spec, ctx, Object.assign({}, options, {
                    pkgId,
                    dependencyShrinkwrap,
                }));
                if (options.keypath && options.keypath.indexOf(pkg.id) !== -1) {
                    return null;
                }
                return pkg;
            }
            catch (err) {
                if (options.optional) {
                    pnpm_logger_1.default.warn({
                        message: `Skipping failed optional dependency ${pkgId || spec.rawSpec}`,
                        err,
                    });
                    return null; // is it OK to return null?
                }
                throw err;
            }
        }))))
            .filter(pkg => pkg);
        return installedPkgs;
    });
}
function isInnerLink(modules, depName) {
    return __awaiter(this, void 0, void 0, function* () {
        let link;
        try {
            link = yield fs.readlink(path.join(modules, depName));
        }
        catch (err) {
            return true;
        }
        const absLink = path.isAbsolute(link) ? link : path.join(modules, link);
        if (absLink.startsWith(modules)) {
            return true;
        }
        pnpm_logger_1.default.info(`${depName} is linked to ${modules} from ${link}`);
        return false;
    });
}
function install(spec, ctx, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const keypath = options.keypath || [];
        const update = keypath.length <= options.depth;
        const fetchedPkg = yield fetch_1.default(spec, Object.assign({}, options, {
            update,
            shrinkwrapResolution: options.dependencyShrinkwrap && options.dependencyShrinkwrap.resolution,
            fetchingLocker: ctx.fetchingLocker,
        }));
        if (keypath.indexOf(fetchedPkg.id) !== -1) {
            return fetchedPkg;
        }
        ctx.shrinkwrap.packages[fetchedPkg.id] = ctx.shrinkwrap.packages[fetchedPkg.id] || {};
        ctx.shrinkwrap.packages[fetchedPkg.id].resolution = fetchedPkg.resolution;
        logFetchStatus(spec.rawSpec, fetchedPkg);
        const pkg = yield fetchedPkg.fetchingPkg;
        if (!options.force) {
            yield isInstallable(pkg, fetchedPkg, options);
        }
        const modules = path.join(options.baseNodeModules, `.${fetchedPkg.id}`, 'node_modules');
        const dependency = Object.assign({}, fetchedPkg, {
            keypath,
            dependencies: [],
            optional: options.optional === true,
            pkg,
            hardlinkedLocation: path.join(modules, pkg.name),
            modules,
        });
        addInstalledPkg(ctx.installs, dependency);
        addToGraph(ctx.graph, keypath[keypath.length - 1], dependency);
        if (!ctx.installed.has(dependency.id)) {
            ctx.installed.add(dependency.id);
            dependency.dependencies = yield installDependencies(pkg, dependency, ctx, modules, Object.assign({}, options, {
                resolvedDependencies: options.dependencyShrinkwrap && options.dependencyShrinkwrap.dependencies
            }));
            if (dependency.dependencies.length) {
                ctx.shrinkwrap.packages[dependency.id].dependencies = dependency.dependencies
                    .reduce((resolutions, dep) => Object.assign(resolutions, {
                    [dep.pkg.name]: dep.id
                }), {});
            }
        }
        yield ctx.linkingLocker(dependency.hardlinkedLocation, function () {
            return __awaiter(this, void 0, void 0, function* () {
                const newlyFetched = yield dependency.fetchingFiles;
                const pkgJsonPath = path.join(dependency.hardlinkedLocation, 'package.json');
                if (newlyFetched || options.force || !(yield exists(pkgJsonPath)) || !(yield pkgLinkedToStore())) {
                    yield link_dir_1.default(dependency.path, dependency.hardlinkedLocation);
                    if (ctx.installationSequence.indexOf(dependency.id) === -1) {
                        ctx.installationSequence.push(dependency.id);
                    }
                }
                const binPath = path.join(dependency.hardlinkedLocation, 'node_modules', '.bin');
                yield linkBins_1.default(modules, binPath, pkg.name);
                // link also the bundled dependencies` bins
                if (pkg.bundledDependencies || pkg.bundleDependencies) {
                    const bundledModules = path.join(dependency.hardlinkedLocation, 'node_modules');
                    yield linkBins_1.default(bundledModules, binPath);
                }
                function pkgLinkedToStore() {
                    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;
                    });
                }
            });
        });
        return dependency;
    });
}
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 logFetchStatus(pkgRawSpec, fetchedPkg) {
    return __awaiter(this, void 0, void 0, function* () {
        const pkg = yield fetchedPkg.fetchingPkg;
        yield fetchedPkg.fetchingFiles;
        logInstallStatus_1.default({ status: 'done', pkg: { rawSpec: pkgRawSpec, name: pkg.name, version: pkg.version } });
    });
}
function addToGraph(graph, dependent, dependency) {
    dependent = dependent || graphController_1.GRAPH_ENTRY;
    graph[dependent] = graph[dependent] || {};
    graph[dependent].dependencies = graph[dependent].dependencies || {};
    updateDependencyResolution(graph, dependent, dependency.pkg.name, dependency.id);
    graph[dependency.id] = graph[dependency.id] || {};
    graph[dependency.id].dependents = graph[dependency.id].dependents || [];
    if (graph[dependency.id].dependents.indexOf(dependent) === -1) {
        graph[dependency.id].dependents.push(dependent);
    }
}
function updateDependencyResolution(graph, dependent, depName, newDepId) {
    if (graph[dependent].dependencies[depName] &&
        graph[dependent].dependencies[depName] !== newDepId) {
        removeIfNoDependents(graph, graph[dependent].dependencies[depName], dependent);
    }
    graph[dependent].dependencies[depName] = newDepId;
}
function removeIfNoDependents(graph, id, removedDependent) {
    if (graph[id] && graph[id].dependents && graph[id].dependents.length === 1 &&
        graph[id].dependents[0] === removedDependent) {
        Object.keys(graph[id].dependencies || {}).forEach(depName => removeIfNoDependents(graph, graph[id].dependencies[depName], id));
        delete graph[id];
    }
}
function isInstallable(pkg, fetchedPkg, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const warn = (yield installChecks.checkPlatform(pkg)) || (yield installChecks.checkEngine(pkg, {
            pnpmVersion: pnpmPkgJson_1.default.version,
            nodeVersion: options.nodeVersion
        }));
        if (!warn)
            return;
        installCheckLogger.warn(warn);
        if (options.engineStrict || options.optional) {
            yield fetchedPkg.abort();
            throw warn;
        }
    });
}
function installDependencies(pkg, dependency, ctx, modules, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const depsInstallOpts = Object.assign({}, opts, {
            keypath: opts.keypath.concat([dependency.id]),
            root: dependency.srcPath,
            fetchingFiles: dependency.fetchingFiles,
        });
        const bundledDeps = pkg.bundleDependencies || pkg.bundledDependencies || [];
        const filterDeps = getNotBundledDeps.bind(null, bundledDeps);
        const deps = filterDeps(pkg.dependencies || {});
        const optionalDeps = filterDeps(pkg.optionalDependencies || {});
        const installedDeps = yield installAll(ctx, deps, optionalDeps, modules, depsInstallOpts);
        return installedDeps;
    });
}
function getNotBundledDeps(bundledDeps, deps) {
    return Object.keys(deps)
        .filter(depName => bundledDeps.indexOf(depName) === -1)
        .reduce((notBundledDeps, depName) => {
        notBundledDeps[depName] = deps[depName];
        return notBundledDeps;
    }, {});
}
function addInstalledPkg(installs, newPkg) {
    if (!installs[newPkg.id]) {
        installs[newPkg.id] = newPkg;
        return;
    }
    installs[newPkg.id].optional = installs[newPkg.id].optional && newPkg.optional;
}
function getRawSpec(name, version) {
    return version === '*' ? name : `${name}@${version}`;
}
//# sourceMappingURL=installMultiple.js.map