"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 RegClient = require("npm-registry-client");
const pnpm_logger_1 = require("pnpm-logger");
const pLimit = require("p-limit");
const npa = require("npm-package-arg");
const pFilter = require("p-filter");
const R = require("ramda");
const safeIsInnerLink_1 = require("../safeIsInnerLink");
const safeReadPkg_1 = require("../fs/safeReadPkg");
const got_1 = require("../network/got");
const getContext_1 = require("./getContext");
const installMultiple_1 = require("../install/installMultiple");
const link_1 = require("../link");
const save_1 = require("../save");
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 pnpm_lockfile_1 = require("pnpm-lockfile");
const shrinkwrap_1 = require("../fs/shrinkwrap");
const modulesController_1 = require("../fs/modulesController");
const memoize_1 = require("../memoize");
const depsToSpecs_1 = require("../depsToSpecs");
function install(maybeOpts) {
    return __awaiter(this, void 0, void 0, function* () {
        const opts = extendOptions_1.default(maybeOpts);
        return lock_1.default(opts.prefix, () => __awaiter(this, void 0, void 0, function* () {
            const installType = 'general';
            const ctx = yield getContext_1.default(opts, installType);
            const installCtx = yield createInstallCmd(opts, ctx.shrinkwrap, ctx.skipped);
            if (!ctx.pkg)
                throw new Error('No package.json found');
            const specs = specsToInstallFromPackage(ctx.pkg, {
                prefix: opts.prefix,
            });
            specs.forEach(spec => {
                if (ctx.shrinkwrap.specifiers && ctx.shrinkwrap.specifiers[spec.name] !== spec.rawSpec) {
                    delete ctx.shrinkwrap.dependencies[spec.name];
                }
            });
            if (opts.lock === false) {
                return run();
            }
            return lock_1.default(ctx.storePath, run, { stale: opts.lockStaleDuration });
            function run() {
                return __awaiter(this, void 0, void 0, function* () {
                    const scripts = !opts.ignoreScripts && ctx.pkg && ctx.pkg.scripts || {};
                    if (scripts['preinstall']) {
                        npmRun('preinstall', ctx.root, opts.userAgent);
                    }
                    yield installInContext(installType, specs, [], ctx, installCtx, opts);
                    if (scripts['postinstall']) {
                        npmRun('postinstall', ctx.root, opts.userAgent);
                    }
                    if (scripts['prepublish']) {
                        npmRun('prepublish', ctx.root, opts.userAgent);
                    }
                });
            }
        }), { stale: opts.lockStaleDuration });
    });
}
exports.install = install;
function specsToInstallFromPackage(pkg, opts) {
    const depsToInstall = depsFromPackage(pkg);
    return depsToSpecs_1.default(depsToInstall, {
        where: opts.prefix,
        optionalDependencies: pkg.optionalDependencies || {},
        devDependencies: pkg.devDependencies || {},
    });
}
function depsFromPackage(pkg) {
    return Object.assign({}, pkg.devDependencies, pkg.dependencies, pkg.optionalDependencies);
}
/**
 * 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* () {
        const opts = extendOptions_1.default(maybeOpts);
        return lock_1.default(opts.prefix, () => __awaiter(this, void 0, void 0, function* () {
            let packagesToInstall = Array.isArray(fuzzyDeps)
                ? argsToSpecs(fuzzyDeps, {
                    defaultTag: opts.tag,
                    where: opts.prefix,
                    dev: opts.saveDev,
                    optional: opts.saveOptional,
                })
                : depsToSpecs_1.similarDepsToSpecs(fuzzyDeps, {
                    where: opts.prefix,
                    dev: opts.saveDev,
                    optional: opts.saveOptional,
                });
            if (!Object.keys(packagesToInstall).length) {
                throw new Error('At least one package has to be installed');
            }
            const installType = 'named';
            const ctx = yield getContext_1.default(opts, installType);
            const installCtx = yield createInstallCmd(opts, ctx.shrinkwrap, ctx.skipped);
            packagesToInstall.forEach(spec => {
                delete ctx.shrinkwrap.dependencies[spec.name];
            });
            if (opts.lock === false) {
                return run();
            }
            return lock_1.default(ctx.storePath, run, { stale: opts.lockStaleDuration });
            function run() {
                return installInContext(installType, packagesToInstall, packagesToInstall.map(spec => spec.name), ctx, installCtx, opts);
            }
        }), { stale: opts.lockStaleDuration });
    });
}
exports.installPkgs = installPkgs;
function argsToSpecs(args, opts) {
    return args
        .map(arg => npa(arg, opts.where))
        .map(spec => {
        if (spec.type === 'tag' && !spec.rawSpec) {
            spec.fetchSpec = opts.defaultTag;
        }
        spec.dev = opts.dev;
        spec.optional = opts.optional;
        return spec;
    });
}
function installInContext(installType, packagesToInstall, newPkgs, ctx, installCtx, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const nodeModulesPath = path.join(ctx.root, 'node_modules');
        const client = new RegClient(adaptConfig(opts));
        const parts = R.partition(spec => newPkgs.indexOf(spec.name) === -1, packagesToInstall);
        const oldSpecs = parts[0];
        const newSpecs = parts[1];
        const installOpts = {
            root: ctx.root,
            storePath: ctx.storePath,
            localRegistry: opts.localRegistry,
            registry: ctx.shrinkwrap.registry,
            force: opts.force,
            depth: opts.update ? opts.depth :
                (R.equals(ctx.shrinkwrap.packages, ctx.privateShrinkwrap.packages) ? opts.repeatInstallDepth : Infinity),
            engineStrict: opts.engineStrict,
            nodeVersion: opts.nodeVersion,
            got: got_1.default(client, {
                networkConcurrency: opts.networkConcurrency,
                rawNpmConfig: opts.rawNpmConfig,
                alwaysAuth: opts.alwaysAuth,
            }),
            metaCache: opts.metaCache,
            resolvedDependencies: Object.assign({}, ctx.shrinkwrap.devDependencies, ctx.shrinkwrap.dependencies, ctx.shrinkwrap.optionalDependencies),
            offline: opts.offline,
            rawNpmConfig: opts.rawNpmConfig,
            nodeModules: nodeModulesPath,
            update: opts.update,
            keypath: [],
            referencedFrom: opts.prefix,
            prefix: opts.prefix,
            parentNodeId: ':/:',
            currentDepth: 0,
        };
        const nonLinkedPkgs = yield pFilter(packagesToInstall, (spec) => !spec.name || safeIsInnerLink_1.default(nodeModulesPath, spec.name));
        const rootPkgs = yield installMultiple_1.default(installCtx, nonLinkedPkgs, installOpts);
        const rootNodeIds = rootPkgs.map(pkg => pkg.nodeId);
        installCtx.nodesToBuild.forEach(nodeToBuild => {
            installCtx.tree[nodeToBuild.nodeId] = {
                nodeId: nodeToBuild.nodeId,
                pkg: nodeToBuild.pkg,
                children: buildTree(installCtx, nodeToBuild.nodeId, nodeToBuild.pkg.id, installCtx.childrenIdsByParentId[nodeToBuild.pkg.id], nodeToBuild.depth + 1, nodeToBuild.installable),
                depth: nodeToBuild.depth,
                installable: nodeToBuild.installable,
            };
        });
        const pkgs = R.props(rootNodeIds, installCtx.tree).map(node => node.pkg);
        let newPkg = ctx.pkg;
        if (installType === 'named') {
            if (!ctx.pkg) {
                throw new Error('Cannot save because no package.json found');
            }
            const pkgJsonPath = path.join(ctx.root, 'package.json');
            const saveType = getSaveType_1.default(opts);
            newPkg = yield save_1.default(pkgJsonPath, pkgs.map(dep => {
                const spec = R.find(spec => spec.raw === dep.specRaw, newSpecs);
                if (!spec)
                    return null;
                return {
                    name: dep.name,
                    saveSpec: getSaveSpec(spec, dep, opts.saveExact)
                };
            }).filter(Boolean), saveType);
        }
        if (newPkg) {
            ctx.shrinkwrap.dependencies = ctx.shrinkwrap.dependencies || {};
            ctx.shrinkwrap.specifiers = ctx.shrinkwrap.specifiers || {};
            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 ref = shrinkwrap_1.pkgIdToRef(dep.id, dep.name, dep.resolution, ctx.shrinkwrap.registry);
                if (dep.dev) {
                    ctx.shrinkwrap.devDependencies = ctx.shrinkwrap.devDependencies || {};
                    ctx.shrinkwrap.devDependencies[dep.name] = ref;
                }
                else if (dep.optional) {
                    ctx.shrinkwrap.optionalDependencies = ctx.shrinkwrap.optionalDependencies || {};
                    ctx.shrinkwrap.optionalDependencies[dep.name] = ref;
                }
                else {
                    ctx.shrinkwrap.dependencies[dep.name] = ref;
                }
                ctx.shrinkwrap.specifiers[dep.name] = getSpecFromPkg(dep.name);
            });
        }
        const result = yield link_1.default(pkgs, rootNodeIds, installCtx.tree, {
            force: opts.force,
            global: opts.global,
            baseNodeModules: nodeModulesPath,
            bin: opts.bin,
            topParents: ctx.pkg
                ? yield getTopParents(R.difference(R.keys(depsFromPackage(ctx.pkg)), newPkgs), nodeModulesPath)
                : [],
            shrinkwrap: ctx.shrinkwrap,
            production: opts.production,
            optional: opts.optional,
            root: ctx.root,
            privateShrinkwrap: ctx.privateShrinkwrap,
            storePath: ctx.storePath,
            skipped: ctx.skipped,
            pkg: newPkg || ctx.pkg,
        });
        yield pnpm_lockfile_1.save(ctx.root, result.shrinkwrap);
        yield modulesController_1.save(path.join(ctx.root, 'node_modules'), {
            packageManager: `${pnpmPkgJson_1.default.name}@${pnpmPkgJson_1.default.version}`,
            storePath: ctx.storePath,
            skipped: Array.from(installCtx.skipped),
        });
        // postinstall hooks
        if (!(opts.ignoreScripts || !result.newPkgResolvedIds || !result.newPkgResolvedIds.length)) {
            const limitChild = pLimit(opts.childConcurrency);
            const linkedPkgsMapValues = R.values(result.linkedPkgsMap);
            yield Promise.all(R.props(result.newPkgResolvedIds, result.linkedPkgsMap)
                .map(pkg => limitChild(() => __awaiter(this, void 0, void 0, function* () {
                try {
                    yield postInstall_1.default(pkg.hardlinkedLocation, installLogger(pkg.id), {
                        userAgent: opts.userAgent
                    });
                }
                catch (err) {
                    if (installCtx.installs[pkg.id].optional) {
                        pnpm_logger_1.default.warn({
                            message: `Skipping failed optional dependency ${pkg.id}`,
                            err,
                        });
                        return;
                    }
                    throw err;
                }
            }))));
        }
        // waiting till the skipped packages are downloaded to the store
        yield Promise.all(R.props(Array.from(installCtx.skipped), installCtx.installs)
            .filter(Boolean)
            .map(pkg => pkg.fetchingFiles));
    });
}
function buildTree(ctx, parentNodeId, parentId, childrenIds, depth, installable) {
    const childrenNodeIds = [];
    for (const childId of childrenIds) {
        if (parentNodeId.indexOf(`:${parentId}:${childId}:`) !== -1) {
            continue;
        }
        const childNodeId = `${parentNodeId}${childId}:`;
        childrenNodeIds.push(childNodeId);
        ctx.tree[childNodeId] = {
            nodeId: childNodeId,
            pkg: ctx.installs[childId],
            children: buildTree(ctx, childNodeId, childId, ctx.childrenIdsByParentId[childId], depth + 1, installable),
            depth,
            installable,
        };
    }
    return childrenNodeIds;
}
function getTopParents(pkgNames, modules) {
    return __awaiter(this, void 0, void 0, function* () {
        const pkgs = yield Promise.all(pkgNames.map(pkgName => path.join(modules, pkgName)).map(safeReadPkg_1.fromDir));
        return pkgs.filter(Boolean).map((pkg) => ({
            name: pkg.name,
            version: pkg.version,
        }));
    });
}
function getSaveSpec(spec, pkg, saveExact) {
    switch (spec.type) {
        case 'version':
        case 'range':
        case 'tag':
            return `${saveExact ? '' : '^'}${pkg.version}`;
        default:
            return spec.saveSpec;
    }
}
function createInstallCmd(opts, shrinkwrap, skipped) {
    return __awaiter(this, void 0, void 0, function* () {
        return {
            installs: {},
            childrenIdsByParentId: {},
            nodesToBuild: [],
            shrinkwrap,
            fetchingLocker: memoize_1.default(opts.fetchingConcurrency),
            skipped,
            tree: {},
        };
    });
}
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, userAgent) {
    const result = runScript_1.sync('npm', ['run', scriptName], {
        cwd: pkgRoot,
        stdio: 'inherit',
        userAgent,
    });
    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 });
    };
}
//# sourceMappingURL=install.js.map