"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 core_loggers_1 = require("@pnpm/core-loggers");
const logger_1 = require("@pnpm/logger");
const utils_1 = require("@pnpm/utils");
const dp = require("dependency-path");
const path = require("path");
const exists = require("path-exists");
const pnpm_shrinkwrap_1 = require("pnpm-shrinkwrap");
const R = require("ramda");
const semver = require("semver");
const encodePkgId_1 = require("./encodePkgId");
const getIsInstallable_1 = require("./getIsInstallable");
const loggers_1 = require("./loggers");
const wantedDepIsLocallyAvailable_1 = require("./wantedDepIsLocallyAvailable");
const ENGINE_NAME = `${process.platform}-${process.arch}-node-${process.version.split('.')[0]}`;
function resolveDependencies(ctx, wantedDependencies, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const resolvedDependencies = options.resolvedDependencies || {};
        const preferedDependencies = options.preferedDependencies || {};
        const update = options.currentDepth <= ctx.updateDepth;
        const extendedWantedDeps = [];
        let proceedAll = options.parentDependsOnPeers;
        for (const wantedDependency of wantedDependencies) {
            let reference = wantedDependency.alias && resolvedDependencies[wantedDependency.alias];
            let proceed = options.parentDependsOnPeers;
            // 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 && wantedDependency.alias && semver.validRange(wantedDependency.pref) !== null && // tslint:disable-line
                preferedDependencies[wantedDependency.alias] &&
                preferedSatisfiesWanted(preferedDependencies[wantedDependency.alias], wantedDependency, ctx.wantedShrinkwrap, {
                    prefix: ctx.prefix,
                })) {
                proceed = true;
                reference = preferedDependencies[wantedDependency.alias];
            }
            const infoFromShrinkwrap = getInfoFromShrinkwrap(ctx.wantedShrinkwrap, ctx.registries.default, reference, wantedDependency.alias);
            if (infoFromShrinkwrap && infoFromShrinkwrap.dependencyShrinkwrap && infoFromShrinkwrap.dependencyShrinkwrap.id) {
                proceedAll = true;
            }
            extendedWantedDeps.push({
                infoFromShrinkwrap,
                proceed,
                reference,
                wantedDependency,
            });
        }
        const resolveDepOpts = {
            currentDepth: options.currentDepth,
            hasManifestInShrinkwrap: options.hasManifestInShrinkwrap,
            keypath: options.keypath,
            localPackages: options.localPackages,
            parentDependsOnPeer: options.parentDependsOnPeers,
            parentIsInstallable: options.parentIsInstallable,
            parentNodeId: options.parentNodeId,
            readPackageHook: options.readPackageHook,
            shamefullyFlatten: options.shamefullyFlatten,
            sideEffectsCache: options.sideEffectsCache,
            update,
        };
        const pkgAddresses = (yield Promise.all(extendedWantedDeps
            .map((extendedWantedDep) => __awaiter(this, void 0, void 0, function* () {
            return resolveDependency(extendedWantedDep.wantedDependency, ctx, Object.assign({}, resolveDepOpts, extendedWantedDep.infoFromShrinkwrap, { proceed: extendedWantedDep.proceed || proceedAll }));
        }))))
            .filter(Boolean);
        return pkgAddresses;
    });
}
exports.default = resolveDependencies;
function preferedSatisfiesWanted(preferredRef, wantedDep, shr, opts) {
    const relDepPath = dp.refToRelative(preferredRef, wantedDep.alias);
    if (relDepPath === null)
        return false;
    const pkgSnapshot = shr.packages && shr.packages[relDepPath];
    if (!pkgSnapshot) {
        logger_1.default.warn({
            message: `Could not find preferred package ${relDepPath} in shrinkwrap`,
            prefix: opts.prefix,
        });
        return false;
    }
    const nameVer = pnpm_shrinkwrap_1.nameVerFromPkgSnapshot(relDepPath, pkgSnapshot);
    return semver.satisfies(nameVer.version, wantedDep.pref, true);
}
function getInfoFromShrinkwrap(shrinkwrap, defaultRegistry, reference, pkgName) {
    if (!reference || !pkgName) {
        return null;
    }
    const relDepPath = dp.refToRelative(reference, pkgName);
    if (!relDepPath) {
        return null;
    }
    const dependencyShrinkwrap = shrinkwrap.packages && shrinkwrap.packages[relDepPath];
    if (dependencyShrinkwrap) {
        const depPath = dp.resolve(defaultRegistry, relDepPath);
        return {
            dependencyShrinkwrap,
            depPath,
            optionalDependencyNames: R.keys(dependencyShrinkwrap.optionalDependencies),
            pkgId: dependencyShrinkwrap.id || depPath,
            relDepPath,
            resolvedDependencies: Object.assign({}, dependencyShrinkwrap.dependencies, dependencyShrinkwrap.optionalDependencies),
            shrinkwrapResolution: pnpm_shrinkwrap_1.pkgSnapshotToResolution(relDepPath, dependencyShrinkwrap, defaultRegistry),
        };
    }
    else {
        return {
            pkgId: dp.resolve(defaultRegistry, relDepPath),
            relDepPath,
        };
    }
}
function resolveDependency(wantedDependency, ctx, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const keypath = options.keypath || [];
        const update = Boolean(options.update ||
            options.localPackages &&
                wantedDepIsLocallyAvailable_1.default(options.localPackages, wantedDependency, { defaultTag: ctx.defaultTag, registry: ctx.registries.default }));
        const proceed = update || options.proceed || !options.shrinkwrapResolution || ctx.force || keypath.length <= ctx.updateDepth
            || options.dependencyShrinkwrap && options.dependencyShrinkwrap.peerDependencies;
        const parentIsInstallable = options.parentIsInstallable === undefined || options.parentIsInstallable;
        if (!options.shamefullyFlatten && !proceed && options.depPath &&
            // if package is not in `node_modules/.shrinkwrap.yaml`
            // we can safely assume that it doesn't exist in `node_modules`
            options.relDepPath && ctx.currentShrinkwrap.packages && ctx.currentShrinkwrap.packages[options.relDepPath] &&
            (yield exists(path.join(ctx.virtualStoreDir, `.${options.depPath}`))) && (options.currentDepth > 0 || wantedDependency.alias && (yield exists(path.join(ctx.modulesDir, wantedDependency.alias))))) {
            return null;
        }
        const scope = wantedDependency.alias && getScope(wantedDependency.alias);
        const registry = normalizeRegistry(scope && ctx.registries[scope] || ctx.registries.default);
        const dependentId = keypath[keypath.length - 1];
        const loggedPkg = {
            dependentId,
            name: wantedDependency.alias,
            rawSpec: wantedDependency.raw,
        };
        core_loggers_1.progressLogger.debug({
            pkg: loggedPkg,
            status: 'installing',
        });
        let pkgResponse;
        try {
            pkgResponse = yield ctx.storeController.requestPackage(wantedDependency, {
                currentPkgId: options.pkgId,
                defaultTag: ctx.defaultTag,
                downloadPriority: -options.currentDepth,
                localPackages: options.localPackages,
                loggedPkg,
                preferredVersions: ctx.preferredVersions,
                prefix: ctx.prefix,
                registry,
                shrinkwrapResolution: options.shrinkwrapResolution,
                sideEffectsCache: options.sideEffectsCache,
                // Unfortunately, even when run with --shrinkwrap-only, we need the *real* package.json
                // so fetching of the tarball cannot be ever avoided. Related issue: https://github.com/pnpm/pnpm/issues/1176
                skipFetch: false,
                update,
                verifyStoreIntegrity: ctx.verifyStoreIntegrity,
            });
        }
        catch (err) {
            if (wantedDependency.optional) {
                core_loggers_1.skippedOptionalDependencyLogger.debug({
                    details: err.toString(),
                    package: {
                        name: wantedDependency.alias,
                        pref: wantedDependency.pref,
                        version: wantedDependency.alias ? wantedDependency.pref : undefined,
                    },
                    parents: getIsInstallable_1.nodeIdToParents(utils_1.createNodeId(options.parentNodeId, 'fake-id'), ctx.resolvedPackagesByPackageId),
                    prefix: ctx.prefix,
                    reason: 'resolution_failure',
                });
                return null;
            }
            throw err;
        }
        pkgResponse.body.id = encodePkgId_1.default(pkgResponse.body.id);
        if (!options.parentDependsOnPeer && !pkgResponse.body.updated && options.update && options.currentDepth >= ctx.updateDepth && options.relDepPath &&
            ctx.currentShrinkwrap.packages && ctx.currentShrinkwrap.packages[options.relDepPath] && !ctx.force) {
            return null;
        }
        if (pkgResponse.body.isLocal) {
            const manifest = pkgResponse.body.manifest || (yield pkgResponse['fetchingRawManifest']); // tslint:disable-line:no-string-literal
            if (options.currentDepth > 0) {
                logger_1.default.warn({
                    message: `Ignoring file dependency because it is not a root dependency ${wantedDependency}`,
                    prefix: ctx.prefix,
                });
            }
            else {
                ctx.linkedDependencies.push({
                    alias: wantedDependency.alias || manifest.name,
                    dev: wantedDependency.dev,
                    id: pkgResponse.body.id,
                    name: manifest.name,
                    normalizedPref: pkgResponse.body.normalizedPref,
                    optional: wantedDependency.optional,
                    resolution: pkgResponse.body.resolution,
                    specRaw: wantedDependency.raw,
                    version: manifest.version,
                });
            }
            core_loggers_1.progressLogger.debug({
                pkgId: pkgResponse.body.id,
                pkgVersion: manifest.version,
                status: 'downloaded_manifest',
            });
            return null;
        }
        // For the root dependency dependentId will be undefined,
        // that's why checking it
        if (dependentId && utils_1.nodeIdContainsSequence(options.parentNodeId, dependentId, pkgResponse.body.id)) {
            return null;
        }
        let pkg;
        let useManifestInfoFromShrinkwrap = false;
        let prepare;
        let hasBin;
        if (options.hasManifestInShrinkwrap && !options.update && options.dependencyShrinkwrap && options.relDepPath
            && !pkgResponse.body.updated) {
            useManifestInfoFromShrinkwrap = true;
            prepare = options.dependencyShrinkwrap.prepare === true;
            hasBin = options.dependencyShrinkwrap.hasBin === true;
            pkg = Object.assign(pnpm_shrinkwrap_1.nameVerFromPkgSnapshot(options.relDepPath, 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 {
            // tslint:disable:no-string-literal
            try {
                pkg = options.readPackageHook
                    ? options.readPackageHook(pkgResponse.body['manifest'] || (yield pkgResponse['fetchingRawManifest']))
                    : pkgResponse.body['manifest'] || (yield pkgResponse['fetchingRawManifest']);
                prepare = Boolean(pkgResponse.body['resolvedVia'] === 'git-repository' && pkg['scripts'] && typeof pkg['scripts']['prepare'] === 'string');
                if (options.dependencyShrinkwrap && options.dependencyShrinkwrap.deprecated && !pkgResponse.body.updated && !pkg.deprecated) {
                    pkg.deprecated = options.dependencyShrinkwrap.deprecated;
                }
                hasBin = Boolean(pkg.bin && !R.isEmpty(pkg.bin) || pkg.directories && pkg.directories.bin);
            }
            catch (err) {
                // tslint:disable:no-empty
                // avoiding unhandled promise rejections
                if (pkgResponse['finishing'])
                    pkgResponse['finishing'].catch(() => { });
                if (pkgResponse['fetchingFiles'])
                    pkgResponse['fetchingFiles'].catch(() => { });
                // tslint:enable:no-empty
                throw err;
            }
            // tslint:enable:no-string-literal
        }
        if (!pkg.name) { // TODO: don't fail on optional dependencies
            const err = new Error(`Can't install ${wantedDependency.raw}: Missing package name`);
            // tslint:disable:no-string-literal
            err['code'] = 'ERR_PNPM_MISSING_PACKAGE_NAME';
            // tslint:enable:no-string-literal
            throw err;
        }
        if (options.currentDepth === 0 && pkgResponse.body.latest && pkgResponse.body.latest !== pkg.version) {
            ctx.outdatedDependencies[pkgResponse.body.id] = pkgResponse.body.latest;
        }
        if (pkg.deprecated) {
            loggers_1.deprecationLogger.debug({
                deprecated: pkg.deprecated,
                depth: options.currentDepth,
                pkgId: pkgResponse.body.id,
                pkgName: pkg.name,
                pkgVersion: pkg.version,
                prefix: ctx.prefix,
            });
        }
        core_loggers_1.progressLogger.debug({
            pkgId: pkgResponse.body.id,
            pkgVersion: pkg.version,
            status: 'downloaded_manifest',
        });
        // using colon as it will never be used inside a package ID
        const nodeId = utils_1.createNodeId(options.parentNodeId, pkgResponse.body.id);
        const currentIsInstallable = (ctx.force ||
            (yield getIsInstallable_1.default(pkgResponse.body.id, pkg, {
                engineStrict: ctx.engineStrict,
                nodeId,
                nodeVersion: ctx.nodeVersion,
                optional: wantedDependency.optional,
                pnpmVersion: ctx.pnpmVersion,
                prefix: ctx.prefix,
                resolvedPackagesByPackageId: ctx.resolvedPackagesByPackageId,
            })));
        const installable = parentIsInstallable && currentIsInstallable;
        if (installable) {
            ctx.skipped.delete(pkgResponse.body.id);
        }
        if (!ctx.resolvedPackagesByPackageId[pkgResponse.body.id]) {
            core_loggers_1.progressLogger.debug({
                pkgId: pkgResponse.body.id,
                status: 'resolving_content',
            });
            // tslint:disable:no-string-literal
            if (pkgResponse['fetchingFiles']) {
                pkgResponse['fetchingFiles']
                    .then((fetchResult) => {
                    core_loggers_1.progressLogger.debug({
                        pkgId: pkgResponse.body.id,
                        status: fetchResult.fromStore
                            ? 'found_in_store' : 'fetched',
                    });
                });
            }
            // tslint:enable:no-string-literal
            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(pkgResponse.body.id);
            }
            const peerDependencies = peerDependenciesWithoutOwn(pkg);
            ctx.resolvedPackagesByPackageId[pkgResponse.body.id] = {
                additionalInfo: {
                    bundledDependencies: pkg.bundledDependencies,
                    bundleDependencies: pkg.bundleDependencies,
                    cpu: pkg.cpu,
                    deprecated: pkg.deprecated,
                    engines: pkg.engines,
                    os: pkg.os,
                    peerDependencies,
                },
                dev: wantedDependency.dev,
                engineCache: !ctx.force && pkgResponse.body.cacheByEngine && pkgResponse.body.cacheByEngine[ENGINE_NAME],
                fetchingFiles: pkgResponse['fetchingFiles'],
                fetchingRawManifest: pkgResponse['fetchingRawManifest'],
                finishing: pkgResponse['finishing'],
                hasBin,
                hasBundledDependencies: !!(pkg.bundledDependencies || pkg.bundleDependencies),
                id: pkgResponse.body.id,
                name: pkg.name,
                optional: wantedDependency.optional,
                optionalDependencies: new Set(R.keys(pkg.optionalDependencies)),
                path: pkgResponse.body.inStoreLocation,
                peerDependencies: peerDependencies || {},
                prepare,
                prod: !wantedDependency.dev && !wantedDependency.optional,
                requiresBuild: options.dependencyShrinkwrap && Boolean(options.dependencyShrinkwrap.requiresBuild),
                resolution: pkgResponse.body.resolution,
                specRaw: wantedDependency.raw,
                version: pkg.version,
            };
            const children = yield resolveDependenciesOfPackage(pkg, ctx, {
                currentDepth: options.currentDepth + 1,
                hasManifestInShrinkwrap: options.hasManifestInShrinkwrap,
                keypath: options.keypath.concat([pkgResponse.body.id]),
                optionalDependencyNames: options.optionalDependencyNames,
                parentDependsOnPeers: Boolean(options.dependencyShrinkwrap && options.dependencyShrinkwrap.id ||
                    Object.keys(pkg.peerDependencies || {}).length),
                parentIsInstallable: installable,
                parentNodeId: nodeId,
                preferedDependencies: pkgResponse.body.updated
                    ? options.resolvedDependencies
                    : undefined,
                readPackageHook: options.readPackageHook,
                resolvedDependencies: pkgResponse.body.updated
                    ? undefined
                    : options.resolvedDependencies,
                shamefullyFlatten: options.shamefullyFlatten,
                sideEffectsCache: options.sideEffectsCache,
                useManifestInfoFromShrinkwrap,
            });
            ctx.childrenByParentId[pkgResponse.body.id] = children.map((child) => ({
                alias: child.alias,
                pkgId: child.pkgId,
            }));
            ctx.dependenciesTree[nodeId] = {
                children: children.reduce((chn, child) => {
                    chn[child.alias] = child.nodeId;
                    return chn;
                }, {}),
                depth: options.currentDepth,
                installable,
                resolvedPackage: ctx.resolvedPackagesByPackageId[pkgResponse.body.id],
            };
        }
        else {
            ctx.resolvedPackagesByPackageId[pkgResponse.body.id].prod = ctx.resolvedPackagesByPackageId[pkgResponse.body.id].prod || !wantedDependency.dev && !wantedDependency.optional;
            ctx.resolvedPackagesByPackageId[pkgResponse.body.id].dev = ctx.resolvedPackagesByPackageId[pkgResponse.body.id].dev || wantedDependency.dev;
            ctx.resolvedPackagesByPackageId[pkgResponse.body.id].optional = ctx.resolvedPackagesByPackageId[pkgResponse.body.id].optional && wantedDependency.optional;
            ctx.pendingNodes.push({
                alias: wantedDependency.alias || pkg.name,
                depth: options.currentDepth,
                installable,
                nodeId,
                resolvedPackage: ctx.resolvedPackagesByPackageId[pkgResponse.body.id],
            });
        }
        // we need this for saving to package.json
        if (options.currentDepth === 0) {
            ctx.resolvedPackagesByPackageId[pkgResponse.body.id].specRaw = wantedDependency.raw;
        }
        return {
            alias: wantedDependency.alias || pkg.name,
            nodeId,
            normalizedPref: options.currentDepth === 0 ? pkgResponse.body.normalizedPref : undefined,
            pkgId: pkgResponse.body.id,
        };
    });
}
function getScope(pkgName) {
    if (pkgName[0] === '@') {
        return pkgName.substr(0, pkgName.indexOf('/'));
    }
    return null;
}
function peerDependenciesWithoutOwn(pkg) {
    if (!pkg.peerDependencies)
        return pkg.peerDependencies;
    const ownDeps = new Set(R.keys(pkg.dependencies).concat(R.keys(pkg.optionalDependencies)));
    const result = {};
    for (const peer of Object.keys(pkg.peerDependencies)) {
        if (ownDeps.has(peer))
            continue;
        result[peer] = pkg.peerDependencies[peer];
    }
    if (R.isEmpty(result))
        return undefined;
    return result;
}
function normalizeRegistry(registry) {
    if (registry.endsWith('/'))
        return registry;
    return `${registry}/`;
}
function resolveDependenciesOfPackage(pkg, ctx, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        let deps = utils_1.getNonDevWantedDependencies(pkg);
        if (opts.hasManifestInShrinkwrap && !deps.length && opts.resolvedDependencies && opts.useManifestInfoFromShrinkwrap) {
            const optionalDependencyNames = opts.optionalDependencyNames || [];
            deps = Object.keys(opts.resolvedDependencies)
                .map((depName) => ({
                alias: depName,
                optional: optionalDependencyNames.indexOf(depName) !== -1,
            }));
        }
        return resolveDependencies(ctx, deps, opts);
    });
}
//# sourceMappingURL=resolveDependencies.js.map