"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 logInstallStatus_1 = require("../logging/logInstallStatus");
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 getContext_1 = require("./getContext");
const installMultiple_1 = require("../install/installMultiple");
const link_1 = require("./link");
const link_2 = require("../link");
const save_1 = require("../save");
const getSaveType_1 = require("../getSaveType");
const postInstall_1 = require("../install/postInstall");
const extendOptions_1 = require("./extendOptions");
const lock_1 = require("./lock");
const pnpm_shrinkwrap_1 = require("pnpm-shrinkwrap");
const shrinkwrap_1 = require("../fs/shrinkwrap");
const modulesController_1 = require("../fs/modulesController");
const depsToSpecs_1 = require("../depsToSpecs");
const shrinkwrapsEqual_1 = require("./shrinkwrapsEqual");
const package_store_1 = require("package-store");
const depsFromPackage_1 = require("../depsFromPackage");
function install(maybeOpts) {
    return __awaiter(this, void 0, void 0, function* () {
        const reporter = maybeOpts && maybeOpts.reporter;
        if (reporter) {
            pnpm_logger_1.streamParser.on('data', reporter);
        }
        const opts = yield extendOptions_1.default(maybeOpts);
        if (!opts.production && opts.optional) {
            throw new Error('Optional dependencies cannot be installed without production dependencies');
        }
        if (opts.lock) {
            yield lock_1.default(opts.prefix, _install, { stale: opts.lockStaleDuration, locks: opts.locks });
        }
        else {
            yield _install();
        }
        if (reporter) {
            pnpm_logger_1.streamParser.removeListener('data', reporter);
        }
        function _install() {
            return __awaiter(this, void 0, void 0, function* () {
                const installType = 'general';
                const ctx = yield getContext_1.default(opts, installType);
                if (!ctx.pkg)
                    throw new Error('No package.json found');
                const specs = specsToInstallFromPackage(ctx.pkg, {
                    prefix: opts.prefix,
                });
                if (ctx.wantedShrinkwrap.specifiers) {
                    ctx.wantedShrinkwrap.dependencies = ctx.wantedShrinkwrap.dependencies || {};
                    ctx.wantedShrinkwrap.devDependencies = ctx.wantedShrinkwrap.devDependencies || {};
                    ctx.wantedShrinkwrap.optionalDependencies = ctx.wantedShrinkwrap.optionalDependencies || {};
                    for (const spec of specs) {
                        if (ctx.wantedShrinkwrap.specifiers[spec.name] !== spec.rawSpec) {
                            delete ctx.wantedShrinkwrap.dependencies[spec.name];
                            delete ctx.wantedShrinkwrap.devDependencies[spec.name];
                            delete ctx.wantedShrinkwrap.optionalDependencies[spec.name];
                        }
                    }
                }
                const scripts = !opts.ignoreScripts && ctx.pkg && ctx.pkg.scripts || {};
                if (scripts['prepublish']) {
                    pnpm_logger_1.default.warn('`prepublish` scripts are deprecated. Use `prepare` for build steps and `prepublishOnly` for upload-only.');
                }
                const scriptsOpts = {
                    rawNpmConfig: opts.rawNpmConfig,
                    modulesDir: path.join(opts.prefix, 'node_modules'),
                    root: opts.prefix,
                    pkgId: opts.prefix,
                    stdio: 'inherit',
                };
                if (scripts['preinstall']) {
                    yield postInstall_1.npmRunScript('preinstall', ctx.pkg, scriptsOpts);
                }
                if (opts.lock === false) {
                    yield run();
                }
                else {
                    yield lock_1.default(ctx.storePath, run, { stale: opts.lockStaleDuration, locks: opts.locks });
                }
                if (scripts['install']) {
                    yield postInstall_1.npmRunScript('install', ctx.pkg, scriptsOpts);
                }
                if (scripts['postinstall']) {
                    yield postInstall_1.npmRunScript('postinstall', ctx.pkg, scriptsOpts);
                }
                if (scripts['prepublish']) {
                    yield postInstall_1.npmRunScript('prepublish', ctx.pkg, scriptsOpts);
                }
                if (scripts['prepare']) {
                    yield postInstall_1.npmRunScript('prepare', ctx.pkg, scriptsOpts);
                }
                function run() {
                    return __awaiter(this, void 0, void 0, function* () {
                        yield installInContext(installType, specs, [], ctx, opts);
                    });
                }
            });
        }
    });
}
exports.install = install;
function specsToInstallFromPackage(pkg, opts) {
    const depsToInstall = depsFromPackage_1.default(pkg);
    return depsToSpecs_1.default(depsToInstall, {
        where: opts.prefix,
        optionalDependencies: pkg.optionalDependencies || {},
        devDependencies: pkg.devDependencies || {},
    });
}
/**
 * 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 reporter = maybeOpts && maybeOpts.reporter;
        if (reporter) {
            pnpm_logger_1.streamParser.on('data', reporter);
        }
        maybeOpts = maybeOpts || {};
        if (maybeOpts.update === undefined)
            maybeOpts.update = true;
        const opts = yield extendOptions_1.default(maybeOpts);
        if (opts.lock) {
            yield lock_1.default(opts.prefix, _installPkgs, { stale: opts.lockStaleDuration, locks: opts.locks });
        }
        else {
            yield _installPkgs();
        }
        if (reporter) {
            pnpm_logger_1.streamParser.removeListener('data', reporter);
        }
        function _installPkgs() {
            return __awaiter(this, void 0, void 0, function* () {
                const installType = 'named';
                const ctx = yield getContext_1.default(opts, installType);
                const existingSpecs = opts.global ? {} : depsFromPackage_1.default(ctx.pkg);
                const saveType = getSaveType_1.default(opts);
                const optionalDependencies = saveType ? {} : ctx.pkg.optionalDependencies || {};
                const devDependencies = saveType ? {} : ctx.pkg.devDependencies || {};
                let packagesToInstall = Array.isArray(fuzzyDeps)
                    ? argsToSpecs(fuzzyDeps, {
                        defaultTag: opts.tag,
                        where: opts.prefix,
                        dev: opts.saveDev,
                        optional: opts.saveOptional,
                        existingSpecs,
                        optionalDependencies,
                        devDependencies,
                    })
                    : depsToSpecs_1.similarDepsToSpecs(fuzzyDeps, {
                        where: opts.prefix,
                        dev: opts.saveDev,
                        optional: opts.saveOptional,
                        existingSpecs,
                        optionalDependencies,
                        devDependencies,
                    });
                if (!Object.keys(packagesToInstall).length) {
                    throw new Error('At least one package has to be installed');
                }
                if (opts.lock === false) {
                    return run();
                }
                return lock_1.default(ctx.storePath, run, { stale: opts.lockStaleDuration, locks: opts.locks });
                function run() {
                    return installInContext(installType, packagesToInstall, packagesToInstall.map(spec => spec.name), ctx, opts);
                }
            });
        }
    });
}
exports.installPkgs = installPkgs;
function argsToSpecs(args, opts) {
    return args
        .map(arg => npa(arg, opts.where))
        .map(spec => {
        if (!spec.rawSpec && opts.existingSpecs[spec.name]) {
            return npa.resolve(spec.name, opts.existingSpecs[spec.name], opts.where);
        }
        if (spec.type === 'tag' && !spec.rawSpec) {
            spec.fetchSpec = opts.defaultTag;
        }
        return spec;
    })
        .map(spec => {
        spec.dev = opts.dev || !!opts.devDependencies[spec.name];
        spec.optional = opts.optional || !!opts.optionalDependencies[spec.name];
        return spec;
    });
}
function installInContext(installType, packagesToInstall, newPkgs, ctx, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        // Unfortunately, the private shrinkwrap file may differ from the public one.
        // A user might run named installations on a project that has a shrinkwrap.yaml file before running a noop install
        const makePartialCurrentShrinkwrap = installType === 'named' && (ctx.existsWantedShrinkwrap && !ctx.existsCurrentShrinkwrap ||
            // TODO: this operation is quite expensive. We'll have to find a better solution to do this.
            // maybe in pnpm v2 it won't be needed. See: https://github.com/pnpm/pnpm/issues/841
            !shrinkwrapsEqual_1.default(ctx.currentShrinkwrap, ctx.wantedShrinkwrap));
        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];
        // This works from minor version 1, so any number is fine
        // also, the shrinkwrapMinorVersion is going to be removed from shrinkwrap v4
        const hasManifestInShrinkwrap = typeof ctx.wantedShrinkwrap.shrinkwrapMinorVersion === 'number';
        const installCtx = {
            installs: {},
            localPackages: [],
            childrenIdsByParentId: {},
            nodesToBuild: [],
            wantedShrinkwrap: ctx.wantedShrinkwrap,
            currentShrinkwrap: ctx.currentShrinkwrap,
            fetchingLocker: {},
            skipped: ctx.skipped,
            tree: {},
            storeIndex: ctx.storeIndex,
            storePath: ctx.storePath,
            registry: ctx.wantedShrinkwrap.registry,
            force: opts.force,
            depth: (function () {
                // This can be remove from shrinkwrap v4
                if (!hasManifestInShrinkwrap) {
                    // The shrinkwrap file has to be updated to contain
                    // the necessary info from package manifests
                    return Infinity;
                }
                if (opts.update) {
                    return opts.depth;
                }
                if (R.equals(ctx.wantedShrinkwrap.packages, ctx.currentShrinkwrap.packages)) {
                    return opts.repeatInstallDepth;
                }
                return Infinity;
            })(),
            prefix: opts.prefix,
            offline: opts.offline,
            rawNpmConfig: opts.rawNpmConfig,
            nodeModules: nodeModulesPath,
            metaCache: opts.metaCache,
            verifyStoreInegrity: opts.verifyStoreIntegrity,
            engineStrict: opts.engineStrict,
            nodeVersion: opts.nodeVersion,
            pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '',
            got: package_store_1.createGot(client, {
                networkConcurrency: opts.networkConcurrency,
                rawNpmConfig: opts.rawNpmConfig,
                alwaysAuth: opts.alwaysAuth,
                registry: opts.registry,
                retries: opts.fetchRetries,
                factor: opts.fetchRetryFactor,
                maxTimeout: opts.fetchRetryMaxtimeout,
                minTimeout: opts.fetchRetryMintimeout,
            }),
        };
        const installOpts = {
            root: ctx.root,
            resolvedDependencies: Object.assign({}, ctx.wantedShrinkwrap.devDependencies, ctx.wantedShrinkwrap.dependencies, ctx.wantedShrinkwrap.optionalDependencies),
            update: opts.update,
            keypath: [],
            parentNodeId: ':/:',
            currentDepth: 0,
            readPackageHook: opts.hooks.readPackage,
            hasManifestInShrinkwrap,
        };
        const nonLinkedPkgs = yield pFilter(packagesToInstall, (spec) => !spec.name || safeIsInnerLink_1.default(nodeModulesPath, spec.name, { storePath: ctx.storePath }));
        const rootPkgs = yield installMultiple_1.default(installCtx, nonLinkedPkgs, installOpts);
        pnpm_logger_1.stageLogger.debug('resolution_done');
        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);
        const pkgsToSave = pkgs.concat(installCtx.localPackages);
        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, pkgsToSave.map(dep => {
                const spec = R.find(spec => spec.raw === dep.specRaw, newSpecs);
                if (!spec)
                    return null;
                return {
                    name: dep.name,
                    saveSpec: getSaveSpec(spec, dep.version, {
                        saveExact: opts.saveExact,
                        savePrefix: opts.savePrefix,
                    })
                };
            }).filter(Boolean), saveType);
        }
        if (newPkg) {
            ctx.wantedShrinkwrap.dependencies = ctx.wantedShrinkwrap.dependencies || {};
            ctx.wantedShrinkwrap.specifiers = ctx.wantedShrinkwrap.specifiers || {};
            ctx.wantedShrinkwrap.optionalDependencies = ctx.wantedShrinkwrap.optionalDependencies || {};
            ctx.wantedShrinkwrap.devDependencies = ctx.wantedShrinkwrap.devDependencies || {};
            const deps = newPkg.dependencies || {};
            const devDeps = newPkg.devDependencies || {};
            const optionalDeps = newPkg.optionalDependencies || {};
            const getSpecFromPkg = (depName) => deps[depName] || devDeps[depName] || optionalDeps[depName];
            for (const dep of pkgsToSave) {
                const ref = shrinkwrap_1.absolutePathToRef(dep.id, dep.name, dep.resolution, ctx.wantedShrinkwrap.registry);
                const isDev = !!devDeps[dep.name];
                const isOptional = !!optionalDeps[dep.name];
                if (isDev) {
                    ctx.wantedShrinkwrap.devDependencies[dep.name] = ref;
                }
                else if (isOptional) {
                    ctx.wantedShrinkwrap.optionalDependencies[dep.name] = ref;
                }
                else {
                    ctx.wantedShrinkwrap.dependencies[dep.name] = ref;
                }
                if (!isDev) {
                    delete ctx.wantedShrinkwrap.devDependencies[dep.name];
                }
                if (!isOptional) {
                    delete ctx.wantedShrinkwrap.optionalDependencies[dep.name];
                }
                if (isDev || isOptional) {
                    delete ctx.wantedShrinkwrap.dependencies[dep.name];
                }
                ctx.wantedShrinkwrap.specifiers[dep.name] = getSpecFromPkg(dep.name);
            }
        }
        const result = yield link_2.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_1.default(ctx.pkg)), newPkgs), nodeModulesPath)
                : [],
            wantedShrinkwrap: ctx.wantedShrinkwrap,
            production: opts.production,
            development: opts.development,
            optional: opts.optional,
            root: ctx.root,
            currentShrinkwrap: ctx.currentShrinkwrap,
            storePath: ctx.storePath,
            skipped: ctx.skipped,
            pkg: newPkg || ctx.pkg,
            independentLeaves: opts.independentLeaves,
            storeIndex: ctx.storeIndex,
            makePartialCurrentShrinkwrap,
            updateShrinkwrapMinorVersion: installType === 'general' || R.isEmpty(ctx.currentShrinkwrap.packages),
        });
        yield Promise.all([
            pnpm_shrinkwrap_1.write(ctx.root, result.wantedShrinkwrap, result.currentShrinkwrap),
            result.currentShrinkwrap.packages === undefined
                ? Promise.resolve()
                : modulesController_1.save(path.join(ctx.root, 'node_modules'), {
                    packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
                    store: ctx.storePath,
                    skipped: Array.from(installCtx.skipped),
                    layoutVersion: modulesController_1.LAYOUT_VERSION,
                    independentLeaves: opts.independentLeaves,
                }),
        ]);
        // 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, {
                        rawNpmConfig: installCtx.rawNpmConfig,
                        initialWD: ctx.root,
                        userAgent: opts.userAgent,
                        pkgId: pkg.id,
                    });
                }
                catch (err) {
                    if (installCtx.installs[pkg.id].optional) {
                        pnpm_logger_1.default.warn({
                            message: `Skipping failed optional dependency ${pkg.id}`,
                            err,
                        });
                        return;
                    }
                    throw err;
                }
            }))));
        }
        if (installCtx.localPackages.length) {
            const linkOpts = Object.assign({}, opts, {
                skipInstall: true,
                linkToBin: opts.bin,
            });
            yield Promise.all(installCtx.localPackages.map((localPackage) => __awaiter(this, void 0, void 0, function* () {
                yield link_1.default(localPackage.resolution.directory, opts.prefix, linkOpts);
                logInstallStatus_1.default({
                    status: 'installed',
                    pkgId: localPackage.id,
                });
            })));
        }
        // 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));
        // waiting till integrities are saved
        yield Promise.all(R.values(installCtx.installs).map(installed => installed.calculatingIntegrity));
        pnpm_logger_1.summaryLogger.info(undefined);
    });
}
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);
        installable = installable && !ctx.skipped.has(childId);
        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, version, opts) {
    switch (spec.type) {
        case 'version':
        case 'range':
        case 'tag':
            if (opts.saveExact)
                return version;
            return `${opts.savePrefix}${version}`;
        default:
            return spec.saveSpec;
    }
}
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
    };
}
//# sourceMappingURL=install.js.map