install-peerdeps
Version:
CLI to automatically install peerDeps
356 lines (337 loc) • 13.9 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
exports.encodePackageName = encodePackageName;
exports.getPackageData = getPackageData;
require("@babel/polyfill");
var _child_process = require("child_process");
var _fs = _interopRequireDefault(require("fs"));
var _semver = require("semver");
var C = _interopRequireWildcard(require("./constants"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/* eslint-disable no-param-reassign, no-shadow, consistent-return */
/**
* Encodes the package name for use in a URL/HTTP request
* @param {string} packageName - the name of the package to encode
*/
function encodePackageName(packageName) {
// Thanks https://github.com/unpkg/npm-http-server/blob/master/modules/RegistryUtils.js
// for scoped modules help
let encodedPackageName;
if (packageName[0] === "@") {
// For the registry URL, the @ doesn't get URL encoded for some reason
encodedPackageName = `@${encodeURIComponent(packageName.substring(1))}`;
} else {
encodedPackageName = encodeURIComponent(packageName);
}
return encodedPackageName;
}
/**
* Spawns a command to the shell
* @param {string} command - the command to spawn; this must be sanitized per https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2#command-injection-via-args-parameter-of-child_processspawn-without-shell-option-enabled-on-windows-cve-2024-27980---high
* @param {array} args - list of arguments to pass to the command
* @returns {Promise} - a Promise which resolves when the install process is finished
*/
const spawnCommand = (command, args) => {
const isWindows = process.platform === "win32";
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
const cmdProcess = (0, _child_process.spawn)(`${command}${isWindows && !command.endsWith(".cmd") ? ".cmd" : ""}`, args, {
cwd: process.cwd(),
// See:
// - https://github.com/nathanhleung/install-peerdeps/issues/252
// - https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2#command-injection-via-args-parameter-of-child_processspawn-without-shell-option-enabled-on-windows-cve-2024-27980---high
shell: isWindows
});
cmdProcess.stdout.on("data", chunk => {
if (chunk instanceof Buffer) {
stdout += chunk.toString("utf8");
} else {
stdout += chunk;
}
});
cmdProcess.stderr.on("data", chunk => {
if (chunk instanceof Buffer) {
stderr += chunk.toString("utf8");
} else {
stderr += chunk;
}
});
cmdProcess.on("error", reject);
cmdProcess.on("exit", code => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(`${[stdout, stderr].filter(Boolean).join("\n\n")}`));
}
});
});
};
/**
* Gets the current Yarn version
* @returns {string} - The current Yarn version
*/
function getYarnVersion() {
return spawnCommand("yarn", ["--version"]).then(it => it.trim());
}
/**
* Parse a registry manifest to get the best matching version
* @param {Object} requestInfo - information needed to make the request for the data
* @param {string} requestInfo.data - the data from the remote registry
* @param {string} requestInfo.version - the version (or version tag) to try to find
* @returns {string} - The best matching version number
*/
function findPackageVersion({
data,
version
}) {
// Get the max satisfying semver version
const versionToInstall = (0, _semver.maxSatisfying)(data.versions, version);
if (versionToInstall) {
return versionToInstall;
}
// When no matching semver, try named tags, like "latest"
if (data["dist-tags"][version]) {
return data["dist-tags"][version];
}
// No match
throw new Error("That version or tag does not exist.");
}
/**
* Gets metadata about the package from the provided registry
* @param {Object} requestInfo - information needed to make the request for the data
* @param {string} requestInfo.packageName - the name of the package
* @param {string} requestInfo.packageManager - the package manager to use
* @param {string} requestInfo.version - the version (or version tag) to attempt to install
* @returns {Promise<Object>} - a Promise which resolves to the JSON response from the registry
*/
function getPackageData({
packageName,
packageManager,
version
}) {
const pkgString = version ? `${packageName}@${version}` : packageName;
return getYarnVersion().then(yarnVersion => {
// Only return `yarnVersion` when we're using Yarn
if (packageManager !== C.yarn) {
return null;
}
return yarnVersion;
}).catch(err => {
if (packageManager === C.yarn) {
throw err;
}
// If we're not trying to install with Yarn, we won't re-throw and instead will ignore the error and continue
return null;
}).then(yarnVersion => {
// In Yarn versions >= 2, the `yarn info` command was replaced with `yarn npm info`
const isUsingYarnGreaterThan1 = yarnVersion && !yarnVersion.startsWith("1.");
const args = [...(isUsingYarnGreaterThan1 ? ["npm", "info"] : ["info"]), pkgString, "--json"];
return spawnCommand(packageManager, args).then(response => {
const parsed = JSON.parse(response);
// Yarn 1 returns with an extra nested { data } that NPM doesn't
return parsed.data || parsed;
});
});
}
/**
* Gets the contents of the package.json for a package at a specific version
* @param {Object} requestInfo - information needed to make the request for the data
* @param {string} requestInfo.packageName - the name of the package
* @param {Boolean} requestInfo.noRegistry - Gets the package dependencies list from the local node_modules instead of remote registry
* @param {string} requestInfo.packageManager - the package manager to use
* @param {string} requestInfo.version - the version (or version tag) to attempt to install. Ignored if an installed version of the package is found in node_modules.
* @returns {Promise<Object>} - a Promise which resolves to the JSON response from the registry
*/
function getPackageJson({
packageName,
noRegistry,
packageManager,
version
}) {
// Local package.json
if (noRegistry) {
if (_fs.default.existsSync(`node_modules/${packageName}`)) {
return Promise.resolve(JSON.parse(_fs.default.readFileSync(`node_modules/${packageName}/package.json`, "utf8")));
}
}
// Remote registry
return getPackageData({
packageName,
packageManager,
version
}).then(data => {
if (Array.isArray(data)) {
// If `version` is a range (like `^1.0.0` as opposed to `1.0.0`), the
// `info` command returns an array of info objects, one for each
// version that matches the range. The last one seems to be the latest
// (at least with `npm info`), so we grab that one.
data = data[data.length - 1];
}
return Promise.resolve(findPackageVersion({
data,
version
}));
}).then(version => {
return getPackageData({
packageName,
packageManager,
version
});
});
}
/**
* Builds the package install string based on the version
* @param {Object} options - information needed to build a package install string
* @param {string} options.name - name of the package
* @param {string} options.version - version string of the package
* @returns {string} - the package name and version formatted for an install command
*/
const getPackageString = ({
name,
version
}) => {
// check for whitespace
if (version.indexOf(" ") >= 0) {
// Semver ranges can have a join of comparator sets
// e.g. '^3.0.2 || ^4.0.0' or '>=1.2.7 <1.3.0'
// Take the last version in the range
const rangeSplit = version.split(" ");
const versionToInstall = rangeSplit[rangeSplit.length - 1];
if (versionToInstall === null) {
return name;
}
return `${name}@${versionToInstall}`;
}
return `${name}@${version}`;
};
/**
* Installs the peer dependencies of the provided packages
* @param {Object} options - options for the install child_process
* @param {string} options.packageName - the name of the package for which to install peer dependencies
* @param {string} options.version - the version of the package
* @param {C.npm | C.yarn | C.pnpm} options.packageManager - the package manager to use (Yarn or npm)
* @param {string} options.noRegistry - Disable going to a remote registry to find a list of peers. Use local node_modules instead
* @param {string} options.dev - whether to install the dependencies as devDependencies
* @param {boolean} options.onlyPeers - whether to install the package itself or only its peers
* @param {boolean} options.silent - whether to save the new dependencies to package.json (NPM only)
* @param {boolean} options.dryRun - whether to actually install the packages or just display
* the resulting command
* @param {Function} cb - the callback to call when the install process is finished
*/
function installPeerDeps({
packageName,
version,
packageManager,
noRegistry,
dev,
global,
onlyPeers,
silent,
dryRun,
extraArgs
}, cb) {
getPackageJson({
packageName,
noRegistry,
packageManager,
version
})
// Catch before .then because the .then is so long
.catch(err => cb(err)).then(data => {
// Get peer dependencies for max satisfying version
let peerDepsVersionMap = data.peerDependencies;
if (!peerDepsVersionMap) {
peerDepsVersionMap = {};
}
if (Object.keys(peerDepsVersionMap).length === 0) {
console.warn("The package you are trying to install has no peer dependencies. Installing anyway.");
}
// Construct packages string with correct versions for install
// If onlyPeers option is true, don't install the package itself,
// only its peers.
let packagesString = onlyPeers ? "" : `${packageName}@${data.version}`;
const packageList = Object.keys(peerDepsVersionMap).map(name => getPackageString({
name,
version: peerDepsVersionMap[name]
}));
if (packageList.length > 0) {
packagesString = `${packagesString} ${packageList.join(" ")}`;
}
// Construct command based on package manager of current project
let globalFlag = packageManager === C.yarn ? "global" : "--global";
if (!global) {
globalFlag = "";
}
const subcommand = packageManager === C.yarn ? "add" : "install";
let devFlag = packageManager === C.yarn ? "--dev" : "--save-dev";
if (!dev) {
devFlag = "";
}
let args = [];
// I know I can push it, but I'll just
// keep concatenating for consistency
// global must preceed add in yarn; npm doesn't care
args = args.concat(globalFlag);
args = args.concat(subcommand);
// See issue #33 - issue with "-0"
function fixPackageName(packageName) {
if (packageName.substr(-2) === "-0") {
// Remove -0
return packageName.substr(0, packageName.length - 2);
}
return `${packageName}`;
}
// If we have spaces in our args spawn()
// cries foul so we'll split the packagesString
// into an array of individual packages
args = args.concat(packagesString.split(" ").map(fixPackageName));
// If devFlag is empty, then we'd be adding an empty arg
// That causes the command to fail
if (devFlag !== "") {
args = args.concat(devFlag);
}
// If we're using NPM, and there's no dev flag,
// and it's not a silent install and it's not a global install
// make sure to save deps in package.json under "dependencies"
if (devFlag === "" && packageManager === C.npm && !silent && !global) {
args = args.concat("--save");
}
// If we are using NPM, and there's no dev flag,
// and it IS a silent install,
// explicitly pass the --no-save flag
// (NPM v5+ defaults to using --save)
if (devFlag === "" &&
// npm and pnpm are generally interchangeable,
// but pnpm doesn't have a --save option (see above)
[C.npm, C.pnpm].includes(packageManager) && silent) {
args = args.concat("--no-save");
}
// Pass extra args through
if (extraArgs !== "") {
args = args.concat(extraArgs);
}
// Remove empty args
// There's a bug with Yarn 1.0 in which an empty arg
// causes the install process to fail with a "malformed
// response from the registry" error
args = args.filter(a => a !== "");
// Show user the command that's running
const commandString = `${packageManager} ${args.join(" ")}\n`;
if (dryRun) {
console.log(`This command would have been run to install ${packageName}@${version}:`);
console.log(commandString);
} else {
console.log(`Installing peerdeps for ${packageName}@${version}.`);
console.log(commandString);
spawnCommand(packageManager, args).then(() => cb(null)).catch(err => cb(err));
}
});
}
// Export for testing
var _default = exports.default = installPeerDeps;
;