"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 rimraf = require("rimraf-then");
const path = require("path");
const RegClient = require("npm-registry-client");
const pnpm_logger_1 = require("pnpm-logger");
const cloneDeep = require("lodash.clonedeep");
const globalBinPath = require("global-bin-path");
const pLimit = require("p-limit");
const npa = require("npm-package-arg");
const got_1 = require("../network/got");
const getContext_1 = require("./getContext");
const installMultiple_1 = require("../install/installMultiple");
const save_1 = require("../save");
const linkPeers_1 = require("../install/linkPeers");
const getSaveType_1 = require("../getSaveType");
const runScript_1 = require("../runScript");
const postInstall_1 = require("../install/postInstall");
const extendOptions_1 = require("./extendOptions");
const pnpmPkgJson_1 = require("../pnpmPkgJson");
const lock_1 = require("./lock");
const graphController_1 = require("../fs/graphController");
const storeController_1 = require("../fs/storeController");
const shrinkwrap_1 = require("../fs/shrinkwrap");
const modulesController_1 = require("../fs/modulesController");
const memoize_1 = require("../memoize");
const linkBins_1 = require("../install/linkBins");
function install(maybeOpts) {
    return __awaiter(this, void 0, void 0, function* () {
        const opts = extendOptions_1.default(maybeOpts);
        const ctx = yield getContext_1.default(opts);
        const installCtx = yield createInstallCmd(opts, ctx.graph, ctx.shrinkwrap);
        if (!ctx.pkg)
            throw new Error('No package.json found');
        const packagesToInstall = Object.assign({}, ctx.pkg.dependencies || {});
        if (!opts.production)
            Object.assign(packagesToInstall, ctx.pkg.devDependencies || {});
        return lock_1.default(ctx.storePath, () => installInContext('general', packagesToInstall, ctx, installCtx, opts), { stale: opts.lockStaleDuration });
    });
}
exports.install = install;
/**
 * Perform installation.
 *
 * @example
 *     install({'lodash': '1.0.0', 'foo': '^2.1.0' }, { silent: true })
 */
function installPkgs(fuzzyDeps, maybeOpts) {
    return __awaiter(this, void 0, void 0, function* () {
        let packagesToInstall = mapify(fuzzyDeps);
        if (!Object.keys(packagesToInstall).length) {
            throw new Error('At least one package has to be installed');
        }
        const opts = extendOptions_1.default(maybeOpts);
        const ctx = yield getContext_1.default(opts);
        const installCtx = yield createInstallCmd(opts, ctx.graph, ctx.shrinkwrap);
        return lock_1.default(ctx.storePath, () => installInContext('named', packagesToInstall, ctx, installCtx, opts), { stale: opts.lockStaleDuration });
    });
}
exports.installPkgs = installPkgs;
function getResolutions(packagesToInstall, resolvedSpecDeps) {
    return Object.keys(packagesToInstall)
        .reduce((resolvedDeps, depName) => {
        const spec = `${depName}@${packagesToInstall[depName]}`;
        if (resolvedSpecDeps[spec]) {
            resolvedDeps[depName] = resolvedSpecDeps[spec];
        }
        return resolvedDeps;
    }, {});
}
function installInContext(installType, packagesToInstall, ctx, installCtx, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        // TODO: ctx.graph should not be muted. installMultiple should return a new graph
        const oldGraph = cloneDeep(ctx.graph);
        const nodeModulesPath = path.join(ctx.root, 'node_modules');
        const client = new RegClient(adaptConfig(opts));
        const optionalDependencies = ctx.pkg && ctx.pkg && ctx.pkg.optionalDependencies || {};
        const resolvedDependencies = installType !== 'general'
            ? undefined
            : getResolutions(Object.assign({}, optionalDependencies, packagesToInstall), ctx.shrinkwrap.dependencies);
        const installOpts = {
            root: ctx.root,
            storePath: ctx.storePath,
            localRegistry: opts.localRegistry,
            force: opts.force,
            depth: opts.depth,
            tag: opts.tag,
            engineStrict: opts.engineStrict,
            nodeVersion: opts.nodeVersion,
            got: got_1.default(client, { networkConcurrency: opts.networkConcurrency }),
            baseNodeModules: nodeModulesPath,
            metaCache: opts.metaCache,
            resolvedDependencies,
        };
        const pkgs = yield installMultiple_1.default(installCtx, packagesToInstall, optionalDependencies, nodeModulesPath, installOpts);
        const binPath = opts.global ? globalBinPath() : path.join(nodeModulesPath, '.bin');
        yield linkBins_1.default(nodeModulesPath, binPath);
        let newPkg = ctx.pkg;
        if (installType === 'named') {
            const saveType = getSaveType_1.default(opts);
            if (saveType) {
                if (!ctx.pkg) {
                    throw new Error('Cannot save because no package.json found');
                }
                const inputNames = Object.keys(packagesToInstall);
                const savedPackages = pkgs.filter((pkg) => inputNames.indexOf(pkg.pkg.name) > -1);
                const pkgJsonPath = path.join(ctx.root, 'package.json');
                newPkg = yield save_1.default(pkgJsonPath, savedPackages, saveType, opts.saveExact);
            }
        }
        if (newPkg) {
            ctx.shrinkwrap.dependencies = ctx.shrinkwrap.dependencies || {};
            const deps = newPkg.dependencies || {};
            const devDeps = newPkg.devDependencies || {};
            const optionalDeps = newPkg.optionalDependencies || {};
            const getSpecFromPkg = (depName) => deps[depName] || devDeps[depName] || optionalDeps[depName];
            pkgs.forEach(dep => {
                const spec = getSpecFromPkg(dep.pkg.name);
                if (spec) {
                    ctx.shrinkwrap.dependencies[`${dep.pkg.name}@${spec}`] = dep.id;
                }
            });
            Object.keys(ctx.shrinkwrap.dependencies)
                .map(npa)
                .filter((depSpec) => getSpecFromPkg(depSpec.name) !== depSpec.rawSpec)
                .map((depSpec) => depSpec.raw)
                .forEach(removedDep => {
                delete ctx.shrinkwrap.dependencies[removedDep];
            });
        }
        const newGraph = Object.assign({}, ctx.graph);
        yield removeOrphanPkgs(oldGraph, newGraph, ctx.root, ctx.storePath);
        yield graphController_1.save(path.join(ctx.root, 'node_modules'), newGraph);
        yield shrinkwrap_1.save(ctx.root, ctx.shrinkwrap);
        if (ctx.isFirstInstallation) {
            yield modulesController_1.save(path.join(ctx.root, 'node_modules'), {
                packageManager: `${pnpmPkgJson_1.default.name}@${pnpmPkgJson_1.default.version}`,
                storePath: ctx.storePath,
            });
        }
        yield linkPeers_1.default(installCtx.installs);
        // postinstall hooks
        if (!(opts.ignoreScripts || !installCtx.installationSequence || !installCtx.installationSequence.length)) {
            const limitChild = pLimit(opts.childConcurrency);
            yield Promise.all(installCtx.installationSequence.map(pkgId => limitChild(() => __awaiter(this, void 0, void 0, function* () {
                try {
                    yield postInstall_1.default(installCtx.installs[pkgId].hardlinkedLocation, installLogger(pkgId));
                }
                catch (err) {
                    if (installCtx.installs[pkgId].optional) {
                        pnpm_logger_1.default.warn({
                            message: `Skipping failed optional dependency ${pkgId}`,
                            err,
                        });
                        return;
                    }
                    throw err;
                }
            }))));
        }
        if (!opts.ignoreScripts && ctx.pkg) {
            const scripts = ctx.pkg && ctx.pkg.scripts || {};
            if (scripts['postinstall']) {
                npmRun('postinstall', ctx.root);
            }
            if (installType === 'general' && scripts['prepublish']) {
                npmRun('prepublish', ctx.root);
            }
        }
    });
}
function removeOrphanPkgs(oldGraphJson, newGraphJson, root, storePath) {
    return __awaiter(this, void 0, void 0, function* () {
        const oldPkgIds = new Set(Object.keys(oldGraphJson));
        const newPkgIds = new Set(Object.keys(newGraphJson));
        const store = (yield storeController_1.read(storePath)) || {};
        const notDependents = difference(oldPkgIds, newPkgIds);
        yield Promise.all(Array.from(notDependents).map(function (notDependent) {
            return __awaiter(this, void 0, void 0, function* () {
                if (store[notDependent]) {
                    store[notDependent].splice(store[notDependent].indexOf(root), 1);
                    if (!store[notDependent].length) {
                        delete store[notDependent];
                        yield rimraf(path.join(storePath, notDependent));
                    }
                }
            });
        }));
        const newDependents = difference(newPkgIds, oldPkgIds);
        newDependents.forEach(newDependent => {
            store[newDependent] = store[newDependent] || [];
            if (store[newDependent].indexOf(root) === -1) {
                store[newDependent].push(root);
            }
        });
        yield storeController_1.save(storePath, store);
    });
}
function difference(setA, setB) {
    const difference = new Set(setA);
    for (const elem of setB) {
        difference.delete(elem);
    }
    return difference;
}
function createInstallCmd(opts, graph, shrinkwrap) {
    return __awaiter(this, void 0, void 0, function* () {
        return {
            installs: {},
            graph,
            shrinkwrap,
            installed: new Set(),
            installationSequence: [],
            fetchingLocker: memoize_1.default(opts.fetchingConcurrency),
            linkingLocker: memoize_1.default(),
        };
    });
}
function adaptConfig(opts) {
    const registryLog = pnpm_logger_1.default('registry');
    return {
        proxy: {
            http: opts.proxy,
            https: opts.httpsProxy,
            localAddress: opts.localAddress
        },
        ssl: {
            certificate: opts.cert,
            key: opts.key,
            ca: opts.ca,
            strict: opts.strictSsl
        },
        retry: {
            count: opts.fetchRetries,
            factor: opts.fetchRetryFactor,
            minTimeout: opts.fetchRetryMintimeout,
            maxTimeout: opts.fetchRetryMaxtimeout
        },
        userAgent: opts.userAgent,
        log: Object.assign({}, registryLog, {
            verbose: registryLog.debug.bind(null, 'http'),
            http: registryLog.debug.bind(null, 'http'),
        }),
        defaultTag: opts.tag
    };
}
function npmRun(scriptName, pkgRoot) {
    const result = runScript_1.sync('npm', ['run', scriptName], {
        cwd: pkgRoot,
        stdio: 'inherit'
    });
    if (result.status !== 0) {
        process.exit(result.status);
    }
}
const lifecycleLogger = pnpm_logger_1.default('lifecycle');
function installLogger(pkgId) {
    return (stream, line) => {
        const logLevel = stream === 'stderr' ? 'error' : 'info';
        lifecycleLogger[logLevel]({ pkgId, line });
    };
}
function mapify(pkgs) {
    if (!pkgs)
        return {};
    if (Array.isArray(pkgs)) {
        return pkgs.reduce((pkgsMap, pkgRequest) => {
            const matches = /(@?[^@]+)@(.*)/.exec(pkgRequest);
            if (!matches) {
                pkgsMap[pkgRequest] = '*';
            }
            else {
                pkgsMap[matches[1]] = matches[2];
            }
            return pkgsMap;
        }, {});
    }
    return pkgs;
}
//# sourceMappingURL=install.js.map