@pnpm/link-bins
Version:
Link bins to node_modules/.bin
252 lines • 10.6 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.linkBins = linkBins;
exports.linkBinsOfPkgsByAliases = linkBinsOfPkgsByAliases;
exports.linkBinsOfPackages = linkBinsOfPackages;
const fs_1 = require("fs");
const module_1 = __importDefault(require("module"));
const path_1 = __importDefault(require("path"));
const error_1 = require("@pnpm/error");
const logger_1 = require("@pnpm/logger");
const manifest_utils_1 = require("@pnpm/manifest-utils");
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 cmd_shim_1 = __importDefault(require("@zkochan/cmd-shim"));
const rimraf_1 = __importDefault(require("@zkochan/rimraf"));
const is_subdir_1 = __importDefault(require("is-subdir"));
const is_windows_1 = __importDefault(require("is-windows"));
const normalize_path_1 = __importDefault(require("normalize-path"));
const p_settle_1 = __importDefault(require("p-settle"));
const isEmpty_1 = __importDefault(require("ramda/src/isEmpty"));
const unnest_1 = __importDefault(require("ramda/src/unnest"));
const groupBy_1 = __importDefault(require("ramda/src/groupBy"));
const partition_1 = __importDefault(require("ramda/src/partition"));
const semver_1 = __importDefault(require("semver"));
const symlink_dir_1 = __importDefault(require("symlink-dir"));
const fix_bin_1 = __importDefault(require("bin-links/lib/fix-bin"));
const binsConflictLogger = (0, logger_1.logger)('bins-conflict');
const IS_WINDOWS = (0, is_windows_1.default)();
const EXECUTABLE_SHEBANG_SUPPORTED = !IS_WINDOWS;
const POWER_SHELL_IS_SUPPORTED = IS_WINDOWS;
async function linkBins(modulesDir, binsDir, opts) {
const allDeps = await (0, read_modules_dir_1.readModulesDir)(modulesDir);
// If the modules dir does not exist, do nothing
if (allDeps === null)
return [];
return linkBinsOfPkgsByAliases(allDeps, binsDir, {
...opts,
modulesDir,
});
}
async function linkBinsOfPkgsByAliases(depsAliases, binsDir, opts) {
const pkgBinOpts = {
allowExoticManifests: false,
...opts,
};
const directDependencies = opts.projectManifest == null
? undefined
: new Set(Object.keys((0, manifest_utils_1.getAllDependenciesFromManifest)(opts.projectManifest)));
const allCmds = (0, unnest_1.default)((await Promise.all(depsAliases
.map((alias) => ({
depDir: path_1.default.resolve(opts.modulesDir, alias),
isDirectDependency: directDependencies?.has(alias),
nodeExecPath: opts.nodeExecPathByAlias?.[alias],
}))
.filter(({ depDir }) => !(0, is_subdir_1.default)(depDir, binsDir)) // Don't link own bins
.map(async ({ depDir, isDirectDependency, nodeExecPath }) => {
const target = (0, normalize_path_1.default)(depDir);
const cmds = await getPackageBins(pkgBinOpts, target, nodeExecPath);
return cmds.map((cmd) => ({ ...cmd, isDirectDependency }));
})))
.filter((cmds) => cmds.length));
const cmdsToLink = directDependencies != null ? preferDirectCmds(allCmds) : allCmds;
return _linkBins(cmdsToLink, binsDir, opts);
}
function preferDirectCmds(allCmds) {
const [directCmds, hoistedCmds] = (0, partition_1.default)((cmd) => cmd.isDirectDependency === true, allCmds);
const usedDirectCmds = new Set(directCmds.map((directCmd) => directCmd.name));
return [
...directCmds,
...hoistedCmds.filter(({ name }) => !usedDirectCmds.has(name)),
];
}
async function linkBinsOfPackages(pkgs, binsTarget, opts = {}) {
if (pkgs.length === 0)
return [];
const allCmds = (0, unnest_1.default)((await Promise.all(pkgs
.map(async (pkg) => getPackageBinsFromManifest(pkg.manifest, pkg.location, pkg.nodeExecPath))))
.filter((cmds) => cmds.length));
return _linkBins(allCmds, binsTarget, opts);
}
async function _linkBins(allCmds, binsDir, opts) {
if (allCmds.length === 0)
return [];
// deduplicate bin names to prevent race conditions (multiple writers for the same file)
allCmds = deduplicateCommands(allCmds, binsDir);
await fs_1.promises.mkdir(binsDir, { recursive: true });
const results = await (0, p_settle_1.default)(allCmds.map(async (cmd) => linkBin(cmd, binsDir, opts)));
// We want to create all commands that we can create before throwing an exception
for (const result of results) {
if (result.isRejected) {
throw result.reason;
}
}
return allCmds.map(cmd => cmd.pkgName);
}
function deduplicateCommands(commands, binsDir) {
const cmdGroups = (0, groupBy_1.default)(cmd => cmd.name, commands);
return Object.values(cmdGroups)
.filter((group) => group !== undefined && group.length !== 0)
.map(group => resolveCommandConflicts(group, binsDir));
}
function resolveCommandConflicts(group, binsDir) {
return group.reduce((a, b) => {
const [chosen, skipped] = compareCommandsInConflict(a, b) >= 0 ? [a, b] : [b, a];
logCommandConflict(chosen, skipped, binsDir);
return chosen;
});
}
function compareCommandsInConflict(a, b) {
if (a.ownName && !b.ownName)
return 1;
if (!a.ownName && b.ownName)
return -1;
if (a.pkgName !== b.pkgName)
return a.pkgName.localeCompare(b.pkgName); // it's pointless to compare versions of 2 different package
return semver_1.default.compare(a.pkgVersion, b.pkgVersion);
}
function logCommandConflict(chosen, skipped, binsDir) {
binsConflictLogger.debug({
binaryName: skipped.name,
binsDir,
linkedPkgName: chosen.pkgName,
linkedPkgVersion: chosen.pkgVersion,
skippedPkgName: skipped.pkgName,
skippedPkgVersion: skipped.pkgVersion,
});
}
async function isFromModules(filename) {
const real = await fs_1.promises.realpath(filename);
return (0, normalize_path_1.default)(real).includes('/node_modules/');
}
async function getPackageBins(opts, target, nodeExecPath) {
const manifest = opts.allowExoticManifests
? await (0, read_project_manifest_1.safeReadProjectManifestOnly)(target)
: await safeReadPkgJson(target);
if (manifest == null) {
// 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 ((0, isEmpty_1.default)(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.PnpmError('INVALID_PACKAGE_NAME', `Package in ${target} must have a name to get bin linked.`);
}
return getPackageBinsFromManifest(manifest, target, nodeExecPath);
}
async function getPackageBinsFromManifest(manifest, pkgDir, nodeExecPath) {
const cmds = await (0, package_bins_1.getBinsFromPackageManifest)(manifest, pkgDir);
return cmds.map((cmd) => ({
...cmd,
ownName: cmd.name === manifest.name,
pkgName: manifest.name,
pkgVersion: manifest.version,
makePowerShellShim: POWER_SHELL_IS_SUPPORTED && manifest.name !== 'pnpm',
nodeExecPath,
}));
}
async function linkBin(cmd, binsDir, opts) {
const externalBinPath = path_1.default.join(binsDir, cmd.name);
if (IS_WINDOWS) {
const exePath = path_1.default.join(binsDir, `${cmd.name}${getExeExtension()}`);
if ((0, fs_1.existsSync)(exePath)) {
(0, logger_1.globalWarn)(`The target bin directory already contains an exe called ${cmd.name}, so removing ${exePath}`);
await (0, rimraf_1.default)(exePath);
}
}
if (opts?.preferSymlinkedExecutables && !IS_WINDOWS && cmd.nodeExecPath == null) {
try {
await (0, symlink_dir_1.default)(cmd.path, externalBinPath);
await (0, fix_bin_1.default)(cmd.path, 0o755);
}
catch (err) { // eslint-disable-line
if (err.code !== 'ENOENT') {
throw err;
}
(0, logger_1.globalWarn)(`Failed to create bin at ${externalBinPath}. ${err.message}`);
}
return;
}
try {
let nodePath;
if (opts?.extraNodePaths?.length) {
nodePath = [];
for (const modulesPath of await getBinNodePaths(cmd.path)) {
if (opts.extraNodePaths.includes(modulesPath))
break;
nodePath.push(modulesPath);
}
nodePath.push(...opts.extraNodePaths);
}
await (0, cmd_shim_1.default)(cmd.path, externalBinPath, {
createPwshFile: cmd.makePowerShellShim,
nodePath,
nodeExecPath: cmd.nodeExecPath,
});
}
catch (err) { // eslint-disable-line
if (err.code !== 'ENOENT') {
throw err;
}
(0, logger_1.globalWarn)(`Failed to create bin at ${externalBinPath}. ${err.message}`);
return;
}
// ensure that bin are executable and not containing
// windows line-endings(CRLF) on the hashbang line
if (EXECUTABLE_SHEBANG_SUPPORTED) {
await (0, fix_bin_1.default)(cmd.path, 0o755);
}
}
function getExeExtension() {
let cmdExtension;
if (process.env.PATHEXT) {
cmdExtension = process.env.PATHEXT
.split(path_1.default.delimiter)
.find(ext => ext.toUpperCase() === '.EXE');
}
return cmdExtension ?? '.exe';
}
async function getBinNodePaths(target) {
const targetDir = path_1.default.dirname(target);
try {
const targetRealPath = await fs_1.promises.realpath(targetDir);
// @ts-expect-error
return module_1.default['_nodeModulePaths'](targetRealPath);
}
catch (err) { // eslint-disable-line
if (err.code !== 'ENOENT') {
throw err;
}
// @ts-expect-error
return module_1.default['_nodeModulePaths'](targetDir);
}
}
async function safeReadPkgJson(pkgDir) {
try {
return await (0, read_package_json_1.readPackageJsonFromDir)(pkgDir);
}
catch (err) { // eslint-disable-line
if (err.code === 'ENOENT') {
return null;
}
throw err;
}
}
//# sourceMappingURL=index.js.map
;