Skip to content

Commit

Permalink
feat(check_peer_dependencies): Added a script which checks that all p…
Browse files Browse the repository at this point in the history
…eer dependencies are satisfied

npx check_peer_dependencies (--yarn | --npm) [--install]
  • Loading branch information
christopherthielen committed Oct 6, 2019
1 parent a3574bd commit 63e6c97
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 0 deletions.
183 changes: 183 additions & 0 deletions check_peer_dependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env node

const fs = require('fs');
const semver = require('semver');
const util = require('./util');
const yargs = require('yargs')
.option('help', {
alias: 'h',
boolean: true,
description: `Print usage information`,
})
.option('yarn', {
boolean: true,
description: `Use yarn package manager`,
})
.option('npm', {
boolean: true,
description: `Use npm package manager`,
})
.option('install', {
boolean: true,
description: 'Install missing or incorrect peerDependencies'
})
.check(argv => {
if (argv.yarn && argv.npm) {
throw new Error('Specify either --yarn or --npm but not both');
} else if (!argv.yarn && !argv.npm) {
throw new Error('Specify either --yarn or --npm');
}
return true;
});

if (yargs.argv.help) {
process.exit(-2);
}

const PACKAGEJSON = 'package.json';
const NODEMODULES = 'node_modules';

const readFile = filename => JSON.parse(fs.readFileSync(filename).toString('utf-8'));

const visitedDeps = [];
const peerDeps = [];

checkInstalledPackage(readFile(PACKAGEJSON));

function getDependencies(packageJson) {
const { name, dependencies = {}, peerDependencies = {} } = packageJson;
return {
name,
dependencies: Object.keys(dependencies).map(name => ({ name, version: dependencies[ name ] })),
peerDependencies: Object.keys(peerDependencies).map(name => ({ name, version: peerDependencies[ name ] })),
};
}


function checkInstalledPackage(packageJson) {
const { name, dependencies, peerDependencies } = getDependencies(packageJson);
if (visitedDeps.includes(name)) {
return;
}
visitedDeps.push(name);

peerDependencies.forEach(peer => peerDeps.push({ ...peer, depender: `${packageJson.name}@${packageJson.version}` }));
dependencies.forEach(dependency => {
const dependencyName = dependency.name;
const installedPkgDir = `${NODEMODULES}/${dependencyName}`;
const nestedPkgJson = `${installedPkgDir}/${PACKAGEJSON}`;
const nestedPkg = readFile(nestedPkgJson);
checkInstalledPackage(nestedPkg)
});
}

console.log('Peer Dependencies:');
peerDeps.forEach(dep => console.log(`${dep.depender} requires ${dep.name} ${dep.version}`));
console.log('');

const missingPeers = [];
const incorrectPeers = [];

function handleMissingPeerDependency(peerDependency) {
missingPeers.push(peerDependency);
}

function handleIncorrectPeerDependency(peerDependency, installedVersion) {
incorrectPeers.push({ ...peerDependency, installedVersion });
}

/**
* Given a peerDependency, checks that the installed version of the package satisfies the required version
*/
function checkPeerDependency(peerDependency) {
const pkgJsonFile = `${NODEMODULES}/${peerDependency.name}/${PACKAGEJSON}`;
const exists = fs.existsSync(pkgJsonFile);
if (!exists) {
return handleMissingPeerDependency(peerDependency);
}

const installedVersion = readFile(pkgJsonFile).version;

if (!semver.satisfies(installedVersion, peerDependency.version)) {
handleIncorrectPeerDependency(peerDependency, installedVersion);
}
}

peerDeps.forEach(checkPeerDependency);

if (missingPeers.length || incorrectPeers.length) {
console.log('Problems found:');
missingPeers.forEach(peer => console.error(`Missing peer dependency: ${peer.name}@${peer.version} depended on by ${peer.depender}`));
incorrectPeers.forEach(peer =>
console.error(`Incorrect peer dependency: ${peer.name}@${peer.installedVersion} installed but ${peer.depender} requires ${peer.version}.`));
console.error();
}

function semverReverseSort(a, b) {
const lt = semver.lt(a, b);
const gt = semver.gt(a, b);
if (!lt && !gt) {
return 0;
} else if (lt) {
return 1;
}
return -1;
}

const adds = [];
const upgrades = [];

function findPossibleResolution(packageName, allPeerDeps, isMissing = false) {
const requiredPeerVersions = allPeerDeps.filter(dep => dep.name === packageName);
const rawVersionsInfo = util._exec(`npm view ${packageName} versions`, true).stdout;
const availableVersions = JSON.parse(rawVersionsInfo.replace(/'/g, '"')).sort(semverReverseSort);

const foundVer = availableVersions.find(ver => requiredPeerVersions.every(peerVer => semver.satisfies(ver, peerVer.version)));
if (!foundVer) {
console.error(`Unable to find a version for ${packageName} that satisfies the following peerDependencies:`);
requiredPeerVersions.forEach(peer => console.error(`${peer.depender} requires ${peer.name} ${peer.version}`));
throw new Error(`Unable to find a version for ${packageName} that satisfies ` +
`the following peerDependencies: ${requiredPeerVersions.map(x => x.version).join()}`);
}

console.log(`found ${packageName}@${foundVer} which satisfies ${requiredPeerVersions.join(',')}`);

(isMissing ? adds : upgrades).push(`${packageName}@${foundVer}`);
}


const missingPackages = missingPeers.map(x => x.name).reduce((acc, x) => acc.includes(x) ? acc : acc.concat(x), []);
const incorrectPackages = incorrectPeers.map(x => x.name).reduce((acc, x) => acc.includes(x) ? acc : acc.concat(x), []);
if (missingPackages.length || incorrectPackages.length) {
console.log('Searching for solutions:');
missingPackages.forEach(peer => findPossibleResolution(peer, peerDeps, true));
incorrectPackages.forEach(peer => findPossibleResolution(peer, peerDeps, false));
console.log();
}

function getCommandLines() {
const commands = [];
if (yargs.argv.yarn) {
if (adds.length) {
commands.push(`yarn add ${adds.join(' ')}`);
}
if (upgrades.length) {
commands.push(`yarn upgrade ${upgrades.join(' ')}`);
}
} else {
commands.push(`npm install ${adds.concat(upgrades).join(' ')}`)
}
return commands;
}

const commandLines = getCommandLines();
if (commandLines.length) {
console.log('Commands:');
commandLines.forEach(command => {
if (yargs.argv.install) {
util._exec(command);
} else {
console.log(command);
}
});
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"bin": {
"artifact_tagging": "./artifact_tagging.js",
"check_peer_dependencies": "./check_peer_dependencies.js",
"ensure_clean_master": "./ensure_clean_master.js",
"generate_docs": "./generate_docs.js",
"modify_sourcemap_paths": "./modify_sourcemap_paths.js",
Expand Down

0 comments on commit 63e6c97

Please sign in to comment.