"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const error_1 = __importDefault(require("@pnpm/error"));
const logger_1 = __importDefault(require("@pnpm/logger"));
const pkgid_to_filename_1 = __importDefault(require("@pnpm/pkgid-to-filename"));
const nodeIdUtils_1 = require("./nodeIdUtils");
const crypto = require("crypto");
const importFrom = require("import-from");
const path = require("path");
const R = require("ramda");
const semver = require("semver");
function default_1(opts) {
    const depGraph = {};
    const pathsByNodeId = {};
    for (const { directNodeIdsByAlias, topParents, rootDir } of opts.projects) {
        const pkgsByName = Object.assign(R.fromPairs(topParents.map(({ name, version }) => [
            name,
            {
                depth: 0,
                version,
            },
        ])), toPkgByName(Object
            .keys(directNodeIdsByAlias)
            .map((alias) => ({
            alias,
            node: opts.dependenciesTree[directNodeIdsByAlias[alias]],
            nodeId: directNodeIdsByAlias[alias],
        }))));
        resolvePeersOfChildren(directNodeIdsByAlias, pkgsByName, {
            dependenciesTree: opts.dependenciesTree,
            depGraph,
            lockfileDir: opts.lockfileDir,
            pathsByNodeId,
            peersCache: new Map(),
            purePkgs: new Set(),
            rootDir,
            strictPeerDependencies: opts.strictPeerDependencies,
            virtualStoreDir: opts.virtualStoreDir,
        });
    }
    R.values(depGraph).forEach((node) => {
        node.children = R.keys(node.children).reduce((acc, alias) => {
            var _a;
            acc[alias] = (_a = pathsByNodeId[node.children[alias]]) !== null && _a !== void 0 ? _a : node.children[alias];
            return acc;
        }, {});
    });
    const dependenciesByProjectId = {};
    for (const { directNodeIdsByAlias, id } of opts.projects) {
        dependenciesByProjectId[id] = R.keys(directNodeIdsByAlias).reduce((rootPathsByAlias, alias) => {
            rootPathsByAlias[alias] = pathsByNodeId[directNodeIdsByAlias[alias]];
            return rootPathsByAlias;
        }, {});
    }
    return {
        dependenciesGraph: depGraph,
        dependenciesByProjectId,
    };
}
exports.default = default_1;
function resolvePeersOfNode(nodeId, parentParentPkgs, ctx) {
    var _a;
    const node = ctx.dependenciesTree[nodeId];
    if (node.depth === -1)
        return { resolvedPeers: {}, missingPeers: [] };
    const resolvedPackage = node.resolvedPackage;
    if (ctx.purePkgs.has(resolvedPackage.depPath) &&
        ctx.depGraph[resolvedPackage.depPath].depth <= node.depth &&
        R.isEmpty(resolvedPackage.peerDependencies)) {
        ctx.pathsByNodeId[nodeId] = resolvedPackage.depPath;
        return { resolvedPeers: {}, missingPeers: [] };
    }
    if (typeof node.children === 'function') {
        node.children = node.children();
    }
    const children = node.children;
    const parentPkgs = R.isEmpty(children)
        ? parentParentPkgs
        : {
            ...parentParentPkgs,
            ...toPkgByName(Object.keys(children).map((alias) => ({
                alias,
                node: ctx.dependenciesTree[children[alias]],
                nodeId: children[alias],
            }))),
        };
    const hit = (_a = ctx.peersCache.get(resolvedPackage.depPath)) === null || _a === void 0 ? void 0 : _a.find((cache) => cache.resolvedPeers
        .every(([name, cachedNodeId]) => {
        var _a;
        const parentPkgNodeId = (_a = parentPkgs[name]) === null || _a === void 0 ? void 0 : _a.nodeId;
        if (!parentPkgNodeId || !cachedNodeId)
            return false;
        if (parentPkgs[name].nodeId === cachedNodeId)
            return true;
        if (ctx.pathsByNodeId[cachedNodeId] &&
            ctx.pathsByNodeId[cachedNodeId] === ctx.pathsByNodeId[parentPkgs[name].nodeId])
            return true;
        const parentDepPath = ctx.dependenciesTree[parentPkgNodeId].resolvedPackage.depPath;
        if (!ctx.purePkgs.has(parentDepPath))
            return false;
        const cachedDepPath = ctx.dependenciesTree[cachedNodeId].resolvedPackage.depPath;
        return parentDepPath === cachedDepPath;
    }) && cache.missingPeers.every((missingPeer) => !parentPkgs[missingPeer]));
    if (hit) {
        ctx.pathsByNodeId[nodeId] = hit.depPath;
        ctx.depGraph[hit.depPath].depth = Math.min(ctx.depGraph[hit.depPath].depth, node.depth);
        return {
            missingPeers: hit.missingPeers,
            resolvedPeers: R.fromPairs(hit.resolvedPeers),
        };
    }
    const { resolvedPeers: unknownResolvedPeersOfChildren, missingPeers: missingPeersOfChildren, } = resolvePeersOfChildren(children, parentPkgs, ctx);
    const { resolvedPeers, missingPeers } = R.isEmpty(resolvedPackage.peerDependencies)
        ? { resolvedPeers: {}, missingPeers: [] }
        : resolvePeers({
            currentDepth: node.depth,
            dependenciesTree: ctx.dependenciesTree,
            nodeId,
            parentPkgs,
            resolvedPackage,
            rootDir: ctx.rootDir,
            strictPeerDependencies: ctx.strictPeerDependencies,
        });
    const allResolvedPeers = Object.assign(unknownResolvedPeersOfChildren, resolvedPeers);
    delete allResolvedPeers[node.resolvedPackage.name];
    const allMissingPeers = Array.from(new Set([...missingPeersOfChildren, ...missingPeers]));
    let modules;
    let depPath;
    const localLocation = path.join(ctx.virtualStoreDir, pkgid_to_filename_1.default(resolvedPackage.depPath, ctx.lockfileDir));
    if (R.isEmpty(allResolvedPeers)) {
        modules = path.join(localLocation, 'node_modules');
        depPath = resolvedPackage.depPath;
    }
    else {
        const peersFolderSuffix = createPeersFolderSuffix(Object.keys(allResolvedPeers).map((alias) => ({
            name: alias,
            version: ctx.dependenciesTree[allResolvedPeers[alias]].resolvedPackage.version,
        })));
        modules = path.join(`${localLocation}${peersFolderSuffix}`, 'node_modules');
        depPath = `${resolvedPackage.depPath}${peersFolderSuffix}`;
    }
    const isPure = R.isEmpty(allResolvedPeers) && allMissingPeers.length === 0;
    if (isPure) {
        ctx.purePkgs.add(resolvedPackage.depPath);
    }
    else {
        const cache = {
            missingPeers: allMissingPeers,
            depPath,
            resolvedPeers: Object.entries(allResolvedPeers),
        };
        if (ctx.peersCache.has(resolvedPackage.depPath)) {
            ctx.peersCache.get(resolvedPackage.depPath).push(cache);
        }
        else {
            ctx.peersCache.set(resolvedPackage.depPath, [cache]);
        }
    }
    ctx.pathsByNodeId[nodeId] = depPath;
    const peerDependencies = { ...resolvedPackage.peerDependencies };
    if (!ctx.depGraph[depPath] || ctx.depGraph[depPath].depth > node.depth) {
        const dir = path.join(modules, resolvedPackage.name);
        const unknownPeers = Object.keys(unknownResolvedPeersOfChildren);
        if (unknownPeers.length) {
            for (const unknownPeer of unknownPeers) {
                if (!peerDependencies[unknownPeer]) {
                    peerDependencies[unknownPeer] = '*';
                }
            }
        }
        ctx.depGraph[depPath] = {
            ...node.resolvedPackage,
            children: Object.assign(getPreviouslyResolvedChildren(nodeId, ctx.dependenciesTree), children, resolvedPeers),
            depPath,
            depth: node.depth,
            dir,
            installable: node.installable,
            isPure,
            modules,
            peerDependencies,
        };
    }
    return { resolvedPeers: allResolvedPeers, missingPeers: allMissingPeers };
}
// When a package has itself in the subdependencies, so there's a cycle,
// pnpm will break the cycle, when it first repeats itself.
// However, when the cycle is broken up, the last repeated package is removed
// from the dependencies of the parent package.
// So we need to merge all the children of all the parent packages with same ID as the resolved package.
// This way we get all the children that were removed, when ending cycles.
function getPreviouslyResolvedChildren(nodeId, dependenciesTree) {
    const parentIds = nodeIdUtils_1.splitNodeId(nodeId);
    const ownId = parentIds.pop();
    const allChildren = {};
    if (!ownId || !parentIds.includes(ownId))
        return allChildren;
    const nodeIdChunks = parentIds.join('>').split(ownId);
    nodeIdChunks.pop();
    nodeIdChunks.reduce((accNodeId, part) => {
        accNodeId += `${part}${ownId}`;
        const parentNode = dependenciesTree[`${accNodeId}>`];
        Object.assign(allChildren, typeof parentNode.children === 'function' ? parentNode.children() : parentNode.children);
        return accNodeId;
    }, '>');
    return allChildren;
}
function resolvePeersOfChildren(children, parentPkgs, ctx) {
    const allResolvedPeers = {};
    const allMissingPeers = new Set();
    for (const childNodeId of R.values(children)) {
        const { resolvedPeers, missingPeers } = resolvePeersOfNode(childNodeId, parentPkgs, ctx);
        Object.assign(allResolvedPeers, resolvedPeers);
        missingPeers.forEach((missingPeer) => allMissingPeers.add(missingPeer));
    }
    const unknownResolvedPeersOfChildren = R.keys(allResolvedPeers)
        .filter((alias) => !children[alias])
        .reduce((acc, peer) => {
        acc[peer] = allResolvedPeers[peer];
        return acc;
    }, {});
    return { resolvedPeers: unknownResolvedPeersOfChildren, missingPeers: Array.from(allMissingPeers) };
}
function resolvePeers(ctx) {
    var _a, _b;
    const resolvedPeers = {};
    const missingPeers = [];
    for (const peerName in ctx.resolvedPackage.peerDependencies) { // eslint-disable-line:forin
        const peerVersionRange = ctx.resolvedPackage.peerDependencies[peerName];
        let resolved = ctx.parentPkgs[peerName];
        if (!resolved) {
            missingPeers.push(peerName);
            try {
                const { version } = importFrom(ctx.rootDir, `${peerName}/package.json`);
                resolved = {
                    depth: -1,
                    version,
                };
            }
            catch (err) {
                if (((_b = (_a = ctx.resolvedPackage.peerDependenciesMeta) === null || _a === void 0 ? void 0 : _a[peerName]) === null || _b === void 0 ? void 0 : _b.optional) === true) {
                    continue;
                }
                const friendlyPath = nodeIdToFriendlyPath(ctx.nodeId, ctx.dependenciesTree);
                const message = `${friendlyPath ? `${friendlyPath}: ` : ''}${packageFriendlyId(ctx.resolvedPackage)} \
requires a peer of ${peerName}@${peerVersionRange} but none was installed.`;
                if (ctx.strictPeerDependencies) {
                    throw new error_1.default('MISSING_PEER_DEPENDENCY', message);
                }
                logger_1.default.warn({
                    message,
                    prefix: ctx.rootDir,
                });
                continue;
            }
        }
        if (!semver.satisfies(resolved.version, peerVersionRange)) {
            const friendlyPath = nodeIdToFriendlyPath(ctx.nodeId, ctx.dependenciesTree);
            const message = `${friendlyPath ? `${friendlyPath}: ` : ''}${packageFriendlyId(ctx.resolvedPackage)} \
requires a peer of ${peerName}@${peerVersionRange} but version ${resolved.version} was installed.`;
            if (ctx.strictPeerDependencies) {
                throw new error_1.default('INVALID_PEER_DEPENDENCY', message);
            }
            logger_1.default.warn({
                message,
                prefix: ctx.rootDir,
            });
        }
        if (resolved === null || resolved === void 0 ? void 0 : resolved.nodeId)
            resolvedPeers[peerName] = resolved.nodeId;
    }
    return { resolvedPeers, missingPeers };
}
function packageFriendlyId(manifest) {
    return `${manifest.name}@${manifest.version}`;
}
function nodeIdToFriendlyPath(nodeId, dependenciesTree) {
    const parts = nodeIdUtils_1.splitNodeId(nodeId).slice(0, -1);
    const result = R.scan((prevNodeId, pkgId) => nodeIdUtils_1.createNodeId(prevNodeId, pkgId), '>', parts)
        .slice(2)
        .map((nid) => dependenciesTree[nid].resolvedPackage.name)
        .join(' > ');
    return result;
}
function toPkgByName(nodes) {
    const pkgsByName = {};
    for (const { alias, node, nodeId } of nodes) {
        pkgsByName[alias] = {
            depth: node.depth,
            nodeId,
            version: node.resolvedPackage.version,
        };
    }
    return pkgsByName;
}
function createPeersFolderSuffix(peers) {
    const folderName = peers.map(({ name, version }) => `${name.replace('/', '+')}@${version}`).sort().join('+');
    // We don't want the folder name to get too long.
    // Otherwise, an ENAMETOOLONG error might happen.
    // see: https://github.com/pnpm/pnpm/issues/977
    //
    // A bigger limit might be fine but the md5 hash will be 32 symbols,
    // so for consistency's sake, we go with 32.
    if (folderName.length > 32) {
        return `_${crypto.createHash('md5').update(folderName).digest('hex')}`;
    }
    return `_${folderName}`;
}
//# sourceMappingURL=resolvePeers.js.map