"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const error_1 = require("@pnpm/error");
const package_bins_1 = require("@pnpm/package-bins");
const read_modules_dir_1 = require("@pnpm/read-modules-dir");
const read_package_json_1 = require("@pnpm/read-package-json");
const read_project_manifest_1 = require("@pnpm/read-project-manifest");
const cmdShim = require("@zkochan/cmd-shim");
const isSubdir = require("is-subdir");
const isWindows = require("is-windows");
const makeDir = require("make-dir");
const Module = require("module");
const fs = require("mz/fs");
const normalizePath = require("normalize-path");
const pSettle = require("p-settle");
const path = require("path");
const R = require("ramda");
const IS_WINDOWS = isWindows();
const EXECUTABLE_SHEBANG_SUPPORTED = !IS_WINDOWS;
const POWER_SHELL_IS_SUPPORTED = IS_WINDOWS;
exports.default = async (modulesDir, binsDir, opts) => {
    const allDeps = await read_modules_dir_1.default(modulesDir);
    // If the modules dir does not exist, do nothing
    if (allDeps === null)
        return;
    const pkgBinOpts = {
        allowExoticManifests: false,
        ...opts,
    };
    const allCmds = R.unnest((await Promise.all(allDeps
        .map((depName) => path.resolve(modulesDir, depName))
        .filter((depDir) => !isSubdir(depDir, binsDir)) // Don't link own bins
        .map((depDir) => normalizePath(depDir))
        .map(getPackageBins.bind(null, pkgBinOpts))))
        .filter((cmds) => cmds.length));
    return linkBins(allCmds, binsDir, opts);
};
async function linkBinsOfPackages(pkgs, binsTarget, opts) {
    if (!pkgs.length)
        return;
    const allCmds = R.unnest((await Promise.all(pkgs
        .map((pkg) => getPackageBinsFromManifest(pkg.manifest, pkg.location))))
        .filter((cmds) => cmds.length));
    return linkBins(allCmds, binsTarget, opts);
}
exports.linkBinsOfPackages = linkBinsOfPackages;
async function linkBins(allCmds, binsDir, opts) {
    if (!allCmds.length)
        return [];
    await makeDir(binsDir);
    const [cmdsWithOwnName, cmdsWithOtherNames] = R.partition(({ ownName }) => ownName, allCmds);
    const results1 = await pSettle(cmdsWithOwnName.map((cmd) => linkBin(cmd, binsDir)));
    const usedNames = R.fromPairs(cmdsWithOwnName.map((cmd) => [cmd.name, cmd.name]));
    const results2 = await pSettle(cmdsWithOtherNames.map((cmd) => {
        if (usedNames[cmd.name]) {
            opts.warn(`Cannot link binary '${cmd.name}' of '${cmd.pkgName}' to '${binsDir}': binary of '${usedNames[cmd.name]}' is already linked`, 'BINARIES_CONFLICT');
            return Promise.resolve(undefined);
        }
        usedNames[cmd.name] = cmd.pkgName;
        return linkBin(cmd, binsDir);
    }));
    // We want to create all commands that we can create before throwing an exception
    for (const result of [...results1, ...results2]) {
        if (result.isRejected) {
            throw result.reason;
        }
    }
    return allCmds.map(cmd => cmd.pkgName);
}
async function isFromModules(filename) {
    const real = await fs.realpath(filename);
    return normalizePath(real).includes('/node_modules/');
}
async function getPackageBins(opts, target) {
    const manifest = opts.allowExoticManifests
        ? await safeReadProjectManifestOnly(target) : await safeReadPkgJson(target);
    if (!manifest) {
        // There's a directory in node_modules without package.json: ${target}.
        // This used to be a warning but it didn't really cause any issues.
        return [];
    }
    if (R.isEmpty(manifest.bin) && !await isFromModules(target)) {
        opts.warn(`Package in ${target} must have a non-empty bin field to get bin linked.`, 'EMPTY_BIN');
    }
    if (typeof manifest.bin === 'string' && !manifest.name) {
        throw new error_1.default('INVALID_PACKAGE_NAME', `Package in ${target} must have a name to get bin linked.`);
    }
    return getPackageBinsFromManifest(manifest, target);
}
async function getPackageBinsFromManifest(manifest, pkgDir) {
    const cmds = await package_bins_1.default(manifest, pkgDir);
    return cmds.map((cmd) => ({
        ...cmd,
        ownName: cmd.name === manifest.name,
        pkgName: manifest.name,
    }));
}
async function linkBin(cmd, binsDir) {
    const externalBinPath = path.join(binsDir, cmd.name);
    if (EXECUTABLE_SHEBANG_SUPPORTED) {
        await fs.chmod(cmd.path, 0o755);
    }
    const nodePath = await getBinNodePaths(cmd.path);
    return cmdShim(cmd.path, externalBinPath, {
        createPwshFile: POWER_SHELL_IS_SUPPORTED,
        nodePath,
    });
}
async function getBinNodePaths(target) {
    const targetRealPath = await fs.realpath(target);
    return R.union(Module._nodeModulePaths(targetRealPath), Module._nodeModulePaths(target));
}
async function safeReadPkgJson(pkgDir) {
    try {
        return await read_package_json_1.fromDir(pkgDir);
    }
    catch (err) {
        if (err.code === 'ENOENT') {
            return null;
        }
        throw err;
    }
}
async function safeReadProjectManifestOnly(projectDir) {
    try {
        return await read_project_manifest_1.readProjectManifestOnly(projectDir);
    }
    catch (err) {
        if (err.code === 'ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND') {
            return null;
        }
        throw err;
    }
}
//# sourceMappingURL=index.js.map