"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 logger_1 = require("@pnpm/logger");
const dp = require("dependency-path");
const get_npm_tarball_url_1 = require("get-npm-tarball-url");
const path = require("path");
const exists = require("path-exists");
const R = require("ramda");
const semver = require("semver");
const url = require("url");
const depsToSpecs_1 = require("./depsToSpecs");
const encodePkgId_1 = require("./encodePkgId");
const getPkgInfoFromShr_1 = require("./getPkgInfoFromShr");
const getIsInstallable_1 = require("./install/getIsInstallable");
const loggers_1 = require("./loggers");
const logInstallStatus_1 = require("./logging/logInstallStatus");
const nodeIdUtils_1 = require("./nodeIdUtils");
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.update && options.currentDepth <= ctx.depth;
        const pkgAddresses = (yield Promise.all(wantedDependencies
            .map((wantedDependency) => __awaiter(this, void 0, void 0, function* () {
            let reference = wantedDependency.alias && resolvedDependencies[wantedDependency.alias];
            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 && wantedDependency.alias && semver.validRange(wantedDependency.pref) !== null &&
                preferedDependencies[wantedDependency.alias] &&
                refSatisfies(preferedDependencies[wantedDependency.alias], wantedDependency.pref)) {
                proceed = true;
                reference = preferedDependencies[wantedDependency.alias];
            }
            return yield install(wantedDependency, ctx, Object.assign({ currentDepth: options.currentDepth, hasManifestInShrinkwrap: options.hasManifestInShrinkwrap, keypath: options.keypath, parentIsInstallable: options.parentIsInstallable, parentNodeId: options.parentNodeId, proceed, readPackageHook: options.readPackageHook, reinstallForFlatten: options.reinstallForFlatten, shamefullyFlatten: options.shamefullyFlatten, sideEffectsCache: options.sideEffectsCache, update }, getInfoFromShrinkwrap(ctx.wantedShrinkwrap, reference, wantedDependency.alias, ctx.registry)));
        }))))
            .filter(Boolean);
        return pkgAddresses;
    });
}
exports.default = resolveDependencies;
// 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 relDepPath = dp.refToRelative(reference, pkgName);
    if (!relDepPath) {
        return null;
    }
    const dependencyShrinkwrap = shrinkwrap.packages && shrinkwrap.packages[relDepPath];
    if (dependencyShrinkwrap) {
        const depPath = dp.resolve(shrinkwrap.registry, relDepPath);
        return {
            depPath,
            dependencyShrinkwrap,
            optionalDependencyNames: R.keys(dependencyShrinkwrap.optionalDependencies),
            pkgId: dependencyShrinkwrap.id || depPath,
            relDepPath,
            resolvedDependencies: Object.assign({}, dependencyShrinkwrap.dependencies, dependencyShrinkwrap.optionalDependencies),
            shrinkwrapResolution: dependencyShrToResolution(relDepPath, dependencyShrinkwrap, shrinkwrap.registry),
        };
    }
    else {
        return {
            pkgId: dp.resolve(shrinkwrap.registry, relDepPath),
            relDepPath,
        };
    }
}
function dependencyShrToResolution(relDepPath, depShr, registry) {
    // tslint:disable:no-string-literal
    if (depShr.resolution['type']) {
        return depShr.resolution;
    }
    if (!depShr.resolution['tarball']) {
        return Object.assign({}, depShr.resolution, { registry: depShr.resolution['registry'] || registry, tarball: getTarball() });
    }
    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(relDepPath);
        if (!parsed['name'] || !parsed['version']) {
            throw new Error(`Couldn't get tarball URL from dependency path ${relDepPath}`);
        }
        return get_npm_tarball_url_1.default(parsed['name'], parsed['version'], { registry });
    }
    // tslint:enable:no-string-literal
}
function install(wantedDependency, 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 (!options.shamefullyFlatten && !options.reinstallForFlatten && !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.nodeModules, `.${options.depPath}`))) && (options.currentDepth > 0 || wantedDependency.alias && (yield exists(path.join(ctx.nodeModules, wantedDependency.alias))))) {
            return null;
        }
        const scope = wantedDependency.alias && getScope(wantedDependency.alias);
        const registry = normalizeRegistry(scope && ctx.rawNpmConfig[`${scope}:registry`] || ctx.registry);
        const dependentId = keypath[keypath.length - 1];
        const loggedPkg = {
            dependentId,
            name: wantedDependency.alias,
            rawSpec: wantedDependency.raw,
        };
        logInstallStatus_1.default({
            pkg: loggedPkg,
            status: 'installing',
        });
        let pkgResponse;
        try {
            pkgResponse = yield ctx.storeController.requestPackage(wantedDependency, {
                currentPkgId: options.pkgId,
                defaultTag: ctx.defaultTag,
                downloadPriority: -options.currentDepth,
                loggedPkg,
                preferredVersions: ctx.preferredVersions,
                prefix: ctx.prefix,
                registry,
                shrinkwrapResolution: options.shrinkwrapResolution,
                sideEffectsCache: options.sideEffectsCache,
                skipFetch: ctx.dryRun,
                update: options.update,
                verifyStoreIntegrity: ctx.verifyStoreInegrity,
            });
        }
        catch (err) {
            if (wantedDependency.optional) {
                logger_1.default.warn({
                    err,
                    message: `Skipping optional dependency ${wantedDependency.raw}. ${err.toString()}`,
                });
                return null;
            }
            throw err;
        }
        pkgResponse.body.id = encodePkgId_1.default(pkgResponse.body.id);
        if (pkgResponse.body.isLocal) {
            const manifest = pkgResponse.body.manifest || (yield pkgResponse['fetchingManifest']); // tslint:disable-line:no-string-literal
            if (options.currentDepth > 0) {
                logger_1.default.warn(`Ignoring file dependency because it is not a root dependency ${wantedDependency}`);
            }
            else {
                ctx.localPackages.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,
                });
            }
            logInstallStatus_1.default({ status: 'downloaded_manifest', pkgId: pkgResponse.body.id, pkgVersion: manifest.version });
            return null;
        }
        // For the root dependency dependentId will be undefined,
        // that's why checking it
        if (dependentId && nodeIdUtils_1.nodeIdContainsSequence(options.parentNodeId, dependentId, pkgResponse.body.id)) {
            return null;
        }
        let pkg;
        let useManifestInfoFromShrinkwrap = false;
        if (options.hasManifestInShrinkwrap && !options.update && options.dependencyShrinkwrap && options.relDepPath
            && !pkgResponse.body.updated) {
            useManifestInfoFromShrinkwrap = true;
            pkg = Object.assign(getPkgInfoFromShr_1.default(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['fetchingManifest']))
                    : pkgResponse.body['manifest'] || (yield pkgResponse['fetchingManifest']);
            }
            catch (err) {
                // tslint:disable:no-empty
                // avoiding unhandled promise rejections
                if (pkgResponse['finishing'])
                    pkgResponse['finishing'].catch((err) => { });
                if (pkgResponse['fetchingFiles'])
                    pkgResponse['fetchingFiles'].catch((err) => { });
                // tslint:enable:no-empty
                throw err;
            }
            // tslint:enable:no-string-literal
        }
        if (options.currentDepth === 0 && pkgResponse.body.latest && pkgResponse.body.latest !== pkg.version) {
            ctx.outdatedPkgs[pkgResponse.body.id] = pkgResponse.body.latest;
        }
        if (pkg.deprecated) {
            loggers_1.deprecationLogger.warn({
                deprecated: pkg.deprecated,
                depth: options.currentDepth,
                pkgId: pkgResponse.body.id,
                pkgName: pkg.name,
                pkgVersion: pkg.version,
            });
        }
        logInstallStatus_1.default({ status: 'downloaded_manifest', pkgId: pkgResponse.body.id, pkgVersion: pkg.version });
        // using colon as it will never be used inside a package ID
        const nodeId = nodeIdUtils_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,
                pkgByPkgId: ctx.pkgByPkgId,
                pnpmVersion: ctx.pnpmVersion,
            })));
        const installable = parentIsInstallable && currentIsInstallable;
        if (installable) {
            ctx.skipped.delete(pkgResponse.body.id);
        }
        if (!ctx.pkgByPkgId[pkgResponse.body.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(pkgResponse.body.id);
            }
            const peerDependencies = peerDependenciesWithoutOwn(pkg);
            ctx.pkgByPkgId[pkgResponse.body.id] = {
                additionalInfo: {
                    bundleDependencies: pkg.bundleDependencies,
                    bundledDependencies: pkg.bundledDependencies,
                    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'],
                finishing: pkgResponse['finishing'],
                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 || {},
                prod: !wantedDependency.dev && !wantedDependency.optional,
                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,
                parentIsInstallable: installable,
                parentNodeId: nodeId,
                preferedDependencies: pkgResponse.body.updated
                    ? options.resolvedDependencies
                    : undefined,
                readPackageHook: options.readPackageHook,
                reinstallForFlatten: options.reinstallForFlatten,
                resolvedDependencies: pkgResponse.body.updated
                    ? undefined
                    : options.resolvedDependencies,
                shamefullyFlatten: options.shamefullyFlatten,
                sideEffectsCache: options.sideEffectsCache,
                update: options.update,
                useManifestInfoFromShrinkwrap,
            });
            ctx.childrenByParentId[pkgResponse.body.id] = children.map((child) => ({
                alias: child.alias,
                pkgId: child.pkgId,
            }));
            ctx.pkgGraph[nodeId] = {
                children: children.reduce((chn, child) => {
                    chn[child.alias] = child.nodeId;
                    return chn;
                }, {}),
                depth: options.currentDepth,
                installable,
                pkg: ctx.pkgByPkgId[pkgResponse.body.id],
            };
        }
        else {
            ctx.pkgByPkgId[pkgResponse.body.id].prod = ctx.pkgByPkgId[pkgResponse.body.id].prod || !wantedDependency.dev && !wantedDependency.optional;
            ctx.pkgByPkgId[pkgResponse.body.id].dev = ctx.pkgByPkgId[pkgResponse.body.id].dev || wantedDependency.dev;
            ctx.pkgByPkgId[pkgResponse.body.id].optional = ctx.pkgByPkgId[pkgResponse.body.id].optional && wantedDependency.optional;
            ctx.nodesToBuild.push({
                alias: wantedDependency.alias || pkg.name,
                depth: options.currentDepth,
                installable,
                nodeId,
                pkg: ctx.pkgByPkgId[pkgResponse.body.id],
            });
        }
        // we need this for saving to package.json
        if (options.currentDepth === 0) {
            ctx.pkgByPkgId[pkgResponse.body.id].specRaw = wantedDependency.raw;
        }
        logInstallStatus_1.default({ status: 'dependencies_installed', pkgId: pkgResponse.body.id });
        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 R.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* () {
        const bundledDeps = pkg.bundleDependencies || pkg.bundledDependencies || [];
        const filterDeps = getNotBundledDeps.bind(null, bundledDeps);
        let deps = depsToSpecs_1.default(filterDeps(Object.assign({}, pkg.optionalDependencies, pkg.dependencies)), {
            devDependencies: {},
            optionalDependencies: pkg.optionalDependencies || {},
        });
        if (opts.hasManifestInShrinkwrap && !deps.length && opts.resolvedDependencies && opts.useManifestInfoFromShrinkwrap) {
            const optionalDependencyNames = opts.optionalDependencyNames || [];
            deps = R.keys(opts.resolvedDependencies)
                .map((depName) => ({
                alias: depName,
                optional: optionalDependencyNames.indexOf(depName) !== -1,
            }));
        }
        return yield resolveDependencies(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=resolveDependencies.js.map