"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 pnpm_logger_1 = require("pnpm-logger");
const R = require("ramda");
const get_npm_tarball_url_1 = require("get-npm-tarball-url");
const exists = require("path-exists");
const url = require("url");
const package_store_1 = require("package-store");
const logInstallStatus_1 = require("../logging/logInstallStatus");
const dp = require("dependency-path");
const depsToSpecs_1 = require("../depsToSpecs");
const getIsInstallable_1 = require("./getIsInstallable");
const getPkgInfoFromShr_1 = require("../getPkgInfoFromShr");
const semver = require("semver");
function installMultiple(ctx, specs, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const resolvedDependencies = options.resolvedDependencies || {};
        const preferedDependencies = options.preferedDependencies || {};
        const update = options.update && options.currentDepth <= ctx.depth;
        const pkgAddresses = (yield Promise.all(specs
            .map((spec) => __awaiter(this, void 0, void 0, function* () {
            let reference = resolvedDependencies[spec.name];
            let proceed = false;
            // If dependencies that were used by the previous version of the package
            // satisfy the newer version's requirements, then pnpm tries to keep
            // the previous dependency.
            // So for example, if foo@1.0.0 had bar@1.0.0 as a dependency
            // and foo was updated to 1.1.0 which depends on bar ^1.0.0
            // then bar@1.0.0 can be reused for foo@1.1.0
            if (!reference && spec.type === 'range' && preferedDependencies[spec.name] &&
                refSatisfies(preferedDependencies[spec.name], spec.fetchSpec)) {
                proceed = true;
                reference = preferedDependencies[spec.name];
            }
            return yield install(spec, ctx, Object.assign({
                keypath: options.keypath,
                parentNodeId: options.parentNodeId,
                currentDepth: options.currentDepth,
                parentIsInstallable: options.parentIsInstallable,
                readPackageHook: options.readPackageHook,
                hasManifestInShrinkwrap: options.hasManifestInShrinkwrap,
                update,
                proceed,
            }, getInfoFromShrinkwrap(ctx.wantedShrinkwrap, reference, spec.name, ctx.registry)));
        }))))
            .filter(Boolean);
        return pkgAddresses;
    });
}
exports.default = installMultiple;
// A reference is not always a version.
// We assume that it does not satisfy the range if it's raw form is not a version
// This logic can be made smarter because
// if the reference is /foo/1.0.0/bar@2.0.0, foo's version if 1.0.0
function refSatisfies(reference, range) {
    try {
        return semver.satisfies(reference, range, true);
    }
    catch (err) {
        return false;
    }
}
function getInfoFromShrinkwrap(shrinkwrap, reference, pkgName, registry) {
    if (!reference || !pkgName) {
        return null;
    }
    const dependencyPath = dp.refToRelative(reference, pkgName);
    if (!dependencyPath) {
        return null;
    }
    const dependencyShrinkwrap = shrinkwrap.packages && shrinkwrap.packages[dependencyPath];
    if (dependencyShrinkwrap) {
        const absoluteDependencyPath = dp.resolve(shrinkwrap.registry, dependencyPath);
        return {
            dependencyPath,
            absoluteDependencyPath,
            dependencyShrinkwrap,
            pkgId: dependencyShrinkwrap.id || absoluteDependencyPath,
            shrinkwrapResolution: dependencyShrToResolution(dependencyPath, dependencyShrinkwrap, shrinkwrap.registry),
            resolvedDependencies: Object.assign({}, dependencyShrinkwrap.dependencies, dependencyShrinkwrap.optionalDependencies),
            optionalDependencyNames: R.keys(dependencyShrinkwrap.optionalDependencies),
        };
    }
    else {
        return {
            dependencyPath,
            pkgId: dp.resolve(shrinkwrap.registry, dependencyPath),
        };
    }
}
function dependencyShrToResolution(dependencyPath, depShr, registry) {
    if (depShr.resolution['type']) {
        return depShr.resolution;
    }
    if (!depShr.resolution['tarball']) {
        return Object.assign({}, depShr.resolution, {
            tarball: getTarball(),
            registry: depShr.resolution['registry'] || registry,
        });
    }
    if (depShr.resolution['tarball'].startsWith('file:')) {
        return depShr.resolution;
    }
    return Object.assign({}, depShr.resolution, {
        tarball: url.resolve(registry, depShr.resolution['tarball'])
    });
    function getTarball() {
        const parsed = dp.parse(dependencyPath);
        if (!parsed['name'] || !parsed['version']) {
            throw new Error(`Couldn't get tarball URL from dependency path ${dependencyPath}`);
        }
        return get_npm_tarball_url_1.default(parsed['name'], parsed['version'], { registry });
    }
}
function install(spec, ctx, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const keypath = options.keypath || [];
        const proceed = options.proceed || !options.shrinkwrapResolution || ctx.force || keypath.length <= ctx.depth;
        const parentIsInstallable = options.parentIsInstallable === undefined || options.parentIsInstallable;
        if (!proceed && options.absoluteDependencyPath &&
            // if package is not in `node_modules/.shrinkwrap.yaml`
            // we can safely assume that it doesn't exist in `node_modules`
            options.dependencyPath && ctx.currentShrinkwrap.packages && ctx.currentShrinkwrap.packages[options.dependencyPath] &&
            (yield exists(path.join(ctx.nodeModules, `.${options.absoluteDependencyPath}`))) && (options.currentDepth > 0 || (yield exists(path.join(ctx.nodeModules, spec.name))))) {
            return null;
        }
        const registry = normalizeRegistry(spec.scope && ctx.rawNpmConfig[`${spec.scope}:registry`] || ctx.registry);
        const dependentId = keypath[keypath.length - 1];
        const loggedPkg = {
            rawSpec: spec.rawSpec,
            name: spec.name,
            dependentId,
        };
        logInstallStatus_1.default({
            status: 'installing',
            pkg: loggedPkg,
        });
        const fetchedPkg = yield package_store_1.fetch(spec, {
            loggedPkg,
            update: options.update,
            fetchingLocker: ctx.fetchingLocker,
            registry,
            prefix: ctx.prefix,
            storePath: ctx.storePath,
            metaCache: ctx.metaCache,
            got: ctx.got,
            shrinkwrapResolution: options.shrinkwrapResolution,
            pkgId: options.pkgId,
            offline: ctx.offline,
            storeIndex: ctx.storeIndex,
            verifyStoreIntegrity: ctx.verifyStoreInegrity,
            downloadPriority: -options.currentDepth,
        });
        if (fetchedPkg.isLocal) {
            const pkg = fetchedPkg.pkg;
            if (options.currentDepth > 0) {
                pnpm_logger_1.default.warn(`Ignoring file dependency because it is not a root dependency ${spec}`);
            }
            else {
                ctx.localPackages.push({
                    id: fetchedPkg.id,
                    specRaw: spec.raw,
                    name: pkg.name,
                    version: pkg.version,
                    dev: spec.dev,
                    optional: spec.optional,
                    resolution: fetchedPkg.resolution,
                });
            }
            logInstallStatus_1.default({ status: 'downloaded_manifest', pkgId: fetchedPkg.id, pkgVersion: pkg.version });
            return null;
        }
        if (options.parentNodeId.indexOf(`:${dependentId}:${fetchedPkg.id}:`) !== -1) {
            return null;
        }
        let pkg;
        let useManifestInfoFromShrinkwrap = false;
        if (options.hasManifestInShrinkwrap && !options.update && options.dependencyShrinkwrap && options.dependencyPath) {
            useManifestInfoFromShrinkwrap = true;
            pkg = Object.assign(getPkgInfoFromShr_1.default(options.dependencyPath, options.dependencyShrinkwrap), options.dependencyShrinkwrap);
            if (pkg.peerDependencies) {
                const deps = pkg.dependencies || {};
                R.keys(pkg.peerDependencies).forEach(peer => {
                    delete deps[peer];
                    if (options.resolvedDependencies) {
                        delete options.resolvedDependencies[peer];
                    }
                });
            }
        }
        else {
            try {
                pkg = options.readPackageHook
                    ? options.readPackageHook(yield fetchedPkg.fetchingPkg)
                    : yield fetchedPkg.fetchingPkg;
            }
            catch (err) {
                // avoiding unhandled promise rejections
                fetchedPkg.calculatingIntegrity.catch(err => { });
                fetchedPkg.fetchingFiles.catch(err => { });
                throw err;
            }
        }
        if (pkg.deprecated) {
            pnpm_logger_1.deprecationLogger.warn({
                pkgName: pkg.name,
                pkgVersion: pkg.version,
                pkgId: fetchedPkg.id,
                deprecated: pkg.deprecated,
                depth: options.currentDepth,
            });
        }
        logInstallStatus_1.default({ status: 'downloaded_manifest', pkgId: fetchedPkg.id, pkgVersion: pkg.version });
        const currentIsInstallable = (ctx.force ||
            (yield getIsInstallable_1.default(fetchedPkg.id, pkg, fetchedPkg, {
                optional: spec.optional,
                engineStrict: ctx.engineStrict,
                nodeVersion: ctx.nodeVersion,
                pnpmVersion: ctx.pnpmVersion,
            })));
        const installable = parentIsInstallable && currentIsInstallable;
        // using colon as it will never be used inside a package ID
        const nodeId = `${options.parentNodeId}${fetchedPkg.id}:`;
        if (installable) {
            ctx.skipped.delete(fetchedPkg.id);
        }
        if (!ctx.installs[fetchedPkg.id]) {
            if (!installable) {
                // optional dependencies are resolved for consistent shrinkwrap.yaml files
                // but installed only on machines that are supported by the package
                ctx.skipped.add(fetchedPkg.id);
            }
            ctx.installs[fetchedPkg.id] = {
                id: fetchedPkg.id,
                resolution: fetchedPkg.resolution,
                optional: spec.optional,
                name: pkg.name,
                version: pkg.version,
                prod: !spec.dev && !spec.optional,
                dev: spec.dev,
                fetchingFiles: fetchedPkg.fetchingFiles,
                calculatingIntegrity: fetchedPkg.calculatingIntegrity,
                path: fetchedPkg.path,
                specRaw: spec.raw,
                peerDependencies: pkg.peerDependencies || {},
                optionalDependencies: new Set(R.keys(pkg.optionalDependencies)),
                hasBundledDependencies: !!(pkg.bundledDependencies || pkg.bundleDependencies),
                pkg,
            };
            const children = yield installDependencies(pkg, spec, ctx, {
                parentIsInstallable: installable,
                currentDepth: options.currentDepth + 1,
                parentNodeId: nodeId,
                keypath: options.keypath.concat([fetchedPkg.id]),
                resolvedDependencies: fetchedPkg.id !== options.pkgId
                    ? undefined
                    : options.resolvedDependencies,
                preferedDependencies: fetchedPkg.id !== options.pkgId
                    ? options.resolvedDependencies
                    : undefined,
                optionalDependencyNames: options.optionalDependencyNames,
                update: options.update,
                readPackageHook: options.readPackageHook,
                hasManifestInShrinkwrap: options.hasManifestInShrinkwrap,
                useManifestInfoFromShrinkwrap,
            });
            ctx.childrenIdsByParentId[fetchedPkg.id] = children.map(child => child.pkgId);
            ctx.tree[nodeId] = {
                nodeId,
                pkg: ctx.installs[fetchedPkg.id],
                children: children.map(child => child.nodeId),
                depth: options.currentDepth,
                installable,
            };
        }
        else {
            ctx.installs[fetchedPkg.id].prod = ctx.installs[fetchedPkg.id].prod || !spec.dev && !spec.optional;
            ctx.installs[fetchedPkg.id].dev = ctx.installs[fetchedPkg.id].dev || spec.dev;
            ctx.installs[fetchedPkg.id].optional = ctx.installs[fetchedPkg.id].optional && spec.optional;
            ctx.nodesToBuild.push({
                nodeId,
                pkg: ctx.installs[fetchedPkg.id],
                depth: options.currentDepth,
                installable,
            });
        }
        // we need this for saving to package.json
        if (options.currentDepth === 0) {
            ctx.installs[fetchedPkg.id].specRaw = spec.raw;
        }
        logInstallStatus_1.default({ status: 'dependencies_installed', pkgId: fetchedPkg.id });
        return {
            nodeId,
            pkgId: fetchedPkg.id,
        };
    });
}
function normalizeRegistry(registry) {
    if (registry.endsWith('/'))
        return registry;
    return `${registry}/`;
}
function installDependencies(pkg, parentSpec, ctx, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const bundledDeps = pkg.bundleDependencies || pkg.bundledDependencies || [];
        const filterDeps = getNotBundledDeps.bind(null, bundledDeps);
        let deps = depsToSpecs_1.default(filterDeps(Object.assign({}, pkg.optionalDependencies, pkg.dependencies)), {
            where: ctx.prefix,
            devDependencies: pkg.devDependencies || {},
            optionalDependencies: pkg.optionalDependencies || {},
        });
        if (opts.hasManifestInShrinkwrap && !deps.length && opts.resolvedDependencies && opts.useManifestInfoFromShrinkwrap) {
            const optionalDependencyNames = opts.optionalDependencyNames || [];
            deps = R.keys(opts.resolvedDependencies)
                .map(depName => ({
                name: depName,
                scope: depName[0] === '@' ? depName.split('/')[0] : null,
                optional: optionalDependencyNames.indexOf(depName) !== -1,
            }));
        }
        return yield installMultiple(ctx, deps, opts);
    });
}
function getNotBundledDeps(bundledDeps, deps) {
    return Object.keys(deps)
        .filter(depName => bundledDeps.indexOf(depName) === -1)
        .reduce((notBundledDeps, depName) => {
        notBundledDeps[depName] = deps[depName];
        return notBundledDeps;
    }, {});
}
//# sourceMappingURL=installMultiple.js.map