UNPKG

@pnpm/link-bins

Version:
252 lines 10.6 kB
"use strict"; 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