"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("@pnpm/constants");
const link_bins_1 = require("@pnpm/link-bins");
const lockfile_utils_1 = require("@pnpm/lockfile-utils");
const logger_1 = require("@pnpm/logger");
const pkgid_to_filename_1 = require("@pnpm/pkgid-to-filename");
const symlink_dependency_1 = require("@pnpm/symlink-dependency");
const dp = require("dependency-path");
const path = require("path");
const R = require("ramda");
async function hoistByLockfile(match, opts) {
    if (!opts.lockfile.packages)
        return {};
    const entryNodes = new Set();
    for (let importerId of Object.keys(opts.lockfile.importers).sort()) {
        const lockfileImporter = opts.lockfile.importers[importerId];
        R.toPairs({
            ...lockfileImporter.devDependencies,
            ...lockfileImporter.dependencies,
            ...lockfileImporter.optionalDependencies,
        })
            .map((pair) => dp.refToRelative(pair[1], pair[0]))
            .filter((nodeId) => nodeId !== null).forEach((relativePath) => { entryNodes.add(relativePath); });
    }
    const deps = await getDependencies(opts.lockfile.packages, Array.from(entryNodes.values()), new Set(), 0, {
        getIndependentPackageLocation: opts.getIndependentPackageLocation,
        lockfileDirectory: opts.lockfileDirectory,
        registries: opts.registries,
        virtualStoreDir: opts.virtualStoreDir,
    });
    const aliasesByDependencyPath = await hoistGraph(deps, opts.lockfile.importers['.'].specifiers, {
        dryRun: false,
        match,
        modulesDir: opts.modulesDir,
    });
    const bin = path.join(opts.modulesDir, '.bin');
    const warn = (message) => logger_1.default.warn({ message, prefix: path.join(opts.modulesDir, '../..') });
    try {
        await link_bins_1.default(opts.modulesDir, bin, { allowExoticManifests: true, warn });
    }
    catch (err) {
        // Some packages generate their commands with lifecycle hooks.
        // At this stage, such commands are not generated yet.
        // For now, we don't hoist such generated commands.
        // Related issue: https://github.com/pnpm/pnpm/issues/2071
    }
    return aliasesByDependencyPath;
}
exports.default = hoistByLockfile;
async function getDependencies(pkgSnapshots, depRelPaths, walked, depth, opts) {
    if (depRelPaths.length === 0)
        return [];
    const deps = [];
    let nextDepRelPaths = [];
    for (const depRelPath of depRelPaths) {
        if (walked.has(depRelPath))
            continue;
        walked.add(depRelPath);
        const pkgSnapshot = pkgSnapshots[depRelPath];
        if (!pkgSnapshot) {
            if (depRelPath.startsWith('link:'))
                continue;
            // It might make sense to fail if the depPath is not in the skipped list from .modules.yaml
            // However, the skipped list currently contains package IDs, not dep paths.
            logger_1.default.debug({ message: `No entry for "${depRelPath}" in ${constants_1.WANTED_LOCKFILE}` });
            continue;
        }
        const absolutePath = dp.resolve(opts.registries, depRelPath);
        const pkgName = lockfile_utils_1.nameVerFromPkgSnapshot(depRelPath, pkgSnapshot).name;
        const modules = path.join(opts.virtualStoreDir, pkgid_to_filename_1.default(absolutePath, opts.lockfileDirectory), 'node_modules');
        const independent = opts.getIndependentPackageLocation && lockfile_utils_1.packageIsIndependent(pkgSnapshot);
        const allDeps = {
            ...pkgSnapshot.dependencies,
            ...pkgSnapshot.optionalDependencies,
        };
        deps.push({
            absolutePath,
            children: Object.keys(allDeps).reduce((children, alias) => {
                children[alias] = dp.refToAbsolute(allDeps[alias], alias, opts.registries);
                return children;
            }, {}),
            depth,
            location: !independent
                ? path.join(modules, pkgName)
                : await opts.getIndependentPackageLocation(pkgSnapshot.id || absolutePath, pkgName),
            name: pkgName,
        });
        nextDepRelPaths = [
            ...nextDepRelPaths,
            ...R.toPairs({
                ...pkgSnapshot.dependencies,
                ...pkgSnapshot.optionalDependencies,
            })
                .map((pair) => dp.refToRelative(pair[1], pair[0]))
                .filter((nodeId) => nodeId !== null),
        ];
    }
    return [
        ...deps,
        ...await getDependencies(pkgSnapshots, nextDepRelPaths, walked, depth + 1, opts),
    ];
}
async function hoistGraph(depNodes, currentSpecifiers, opts) {
    const hoistedAliases = new Set(R.keys(currentSpecifiers));
    const aliasesByDependencyPath = {};
    await Promise.all(depNodes
        // sort by depth and then alphabetically
        .sort((a, b) => {
        const depthDiff = a.depth - b.depth;
        return depthDiff === 0 ? a.name.localeCompare(b.name) : depthDiff;
    })
        // build the alias map and the id map
        .map((depNode) => {
        for (const childAlias of Object.keys(depNode.children)) {
            if (!opts.match(childAlias))
                continue;
            // if this alias has already been taken, skip it
            if (hoistedAliases.has(childAlias)) {
                continue;
            }
            hoistedAliases.add(childAlias);
            const childPath = depNode.children[childAlias];
            if (!aliasesByDependencyPath[childPath]) {
                aliasesByDependencyPath[childPath] = [];
            }
            aliasesByDependencyPath[childPath].push(childAlias);
        }
        return depNode;
    })
        .map(async (depNode) => {
        const pkgAliases = aliasesByDependencyPath[depNode.absolutePath];
        if (!pkgAliases) {
            return;
        }
        // TODO when putting logs back in for hoisted packages, you've to put back the condition inside the map,
        // TODO look how it is done in linkPackages
        if (!opts.dryRun) {
            await Promise.all(pkgAliases.map(async (pkgAlias) => {
                await symlink_dependency_1.default(depNode.location, opts.modulesDir, pkgAlias);
            }));
        }
    }));
    return aliasesByDependencyPath;
}
