Skip to content

Commit 63e6c97

Browse files
feat(check_peer_dependencies): Added a script which checks that all peer dependencies are satisfied
npx check_peer_dependencies (--yarn | --npm) [--install]
1 parent a3574bd commit 63e6c97

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

check_peer_dependencies.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const semver = require('semver');
5+
const util = require('./util');
6+
const yargs = require('yargs')
7+
.option('help', {
8+
alias: 'h',
9+
boolean: true,
10+
description: `Print usage information`,
11+
})
12+
.option('yarn', {
13+
boolean: true,
14+
description: `Use yarn package manager`,
15+
})
16+
.option('npm', {
17+
boolean: true,
18+
description: `Use npm package manager`,
19+
})
20+
.option('install', {
21+
boolean: true,
22+
description: 'Install missing or incorrect peerDependencies'
23+
})
24+
.check(argv => {
25+
if (argv.yarn && argv.npm) {
26+
throw new Error('Specify either --yarn or --npm but not both');
27+
} else if (!argv.yarn && !argv.npm) {
28+
throw new Error('Specify either --yarn or --npm');
29+
}
30+
return true;
31+
});
32+
33+
if (yargs.argv.help) {
34+
process.exit(-2);
35+
}
36+
37+
const PACKAGEJSON = 'package.json';
38+
const NODEMODULES = 'node_modules';
39+
40+
const readFile = filename => JSON.parse(fs.readFileSync(filename).toString('utf-8'));
41+
42+
const visitedDeps = [];
43+
const peerDeps = [];
44+
45+
checkInstalledPackage(readFile(PACKAGEJSON));
46+
47+
function getDependencies(packageJson) {
48+
const { name, dependencies = {}, peerDependencies = {} } = packageJson;
49+
return {
50+
name,
51+
dependencies: Object.keys(dependencies).map(name => ({ name, version: dependencies[ name ] })),
52+
peerDependencies: Object.keys(peerDependencies).map(name => ({ name, version: peerDependencies[ name ] })),
53+
};
54+
}
55+
56+
57+
function checkInstalledPackage(packageJson) {
58+
const { name, dependencies, peerDependencies } = getDependencies(packageJson);
59+
if (visitedDeps.includes(name)) {
60+
return;
61+
}
62+
visitedDeps.push(name);
63+
64+
peerDependencies.forEach(peer => peerDeps.push({ ...peer, depender: `${packageJson.name}@${packageJson.version}` }));
65+
dependencies.forEach(dependency => {
66+
const dependencyName = dependency.name;
67+
const installedPkgDir = `${NODEMODULES}/${dependencyName}`;
68+
const nestedPkgJson = `${installedPkgDir}/${PACKAGEJSON}`;
69+
const nestedPkg = readFile(nestedPkgJson);
70+
checkInstalledPackage(nestedPkg)
71+
});
72+
}
73+
74+
console.log('Peer Dependencies:');
75+
peerDeps.forEach(dep => console.log(`${dep.depender} requires ${dep.name} ${dep.version}`));
76+
console.log('');
77+
78+
const missingPeers = [];
79+
const incorrectPeers = [];
80+
81+
function handleMissingPeerDependency(peerDependency) {
82+
missingPeers.push(peerDependency);
83+
}
84+
85+
function handleIncorrectPeerDependency(peerDependency, installedVersion) {
86+
incorrectPeers.push({ ...peerDependency, installedVersion });
87+
}
88+
89+
/**
90+
* Given a peerDependency, checks that the installed version of the package satisfies the required version
91+
*/
92+
function checkPeerDependency(peerDependency) {
93+
const pkgJsonFile = `${NODEMODULES}/${peerDependency.name}/${PACKAGEJSON}`;
94+
const exists = fs.existsSync(pkgJsonFile);
95+
if (!exists) {
96+
return handleMissingPeerDependency(peerDependency);
97+
}
98+
99+
const installedVersion = readFile(pkgJsonFile).version;
100+
101+
if (!semver.satisfies(installedVersion, peerDependency.version)) {
102+
handleIncorrectPeerDependency(peerDependency, installedVersion);
103+
}
104+
}
105+
106+
peerDeps.forEach(checkPeerDependency);
107+
108+
if (missingPeers.length || incorrectPeers.length) {
109+
console.log('Problems found:');
110+
missingPeers.forEach(peer => console.error(`Missing peer dependency: ${peer.name}@${peer.version} depended on by ${peer.depender}`));
111+
incorrectPeers.forEach(peer =>
112+
console.error(`Incorrect peer dependency: ${peer.name}@${peer.installedVersion} installed but ${peer.depender} requires ${peer.version}.`));
113+
console.error();
114+
}
115+
116+
function semverReverseSort(a, b) {
117+
const lt = semver.lt(a, b);
118+
const gt = semver.gt(a, b);
119+
if (!lt && !gt) {
120+
return 0;
121+
} else if (lt) {
122+
return 1;
123+
}
124+
return -1;
125+
}
126+
127+
const adds = [];
128+
const upgrades = [];
129+
130+
function findPossibleResolution(packageName, allPeerDeps, isMissing = false) {
131+
const requiredPeerVersions = allPeerDeps.filter(dep => dep.name === packageName);
132+
const rawVersionsInfo = util._exec(`npm view ${packageName} versions`, true).stdout;
133+
const availableVersions = JSON.parse(rawVersionsInfo.replace(/'/g, '"')).sort(semverReverseSort);
134+
135+
const foundVer = availableVersions.find(ver => requiredPeerVersions.every(peerVer => semver.satisfies(ver, peerVer.version)));
136+
if (!foundVer) {
137+
console.error(`Unable to find a version for ${packageName} that satisfies the following peerDependencies:`);
138+
requiredPeerVersions.forEach(peer => console.error(`${peer.depender} requires ${peer.name} ${peer.version}`));
139+
throw new Error(`Unable to find a version for ${packageName} that satisfies ` +
140+
`the following peerDependencies: ${requiredPeerVersions.map(x => x.version).join()}`);
141+
}
142+
143+
console.log(`found ${packageName}@${foundVer} which satisfies ${requiredPeerVersions.join(',')}`);
144+
145+
(isMissing ? adds : upgrades).push(`${packageName}@${foundVer}`);
146+
}
147+
148+
149+
const missingPackages = missingPeers.map(x => x.name).reduce((acc, x) => acc.includes(x) ? acc : acc.concat(x), []);
150+
const incorrectPackages = incorrectPeers.map(x => x.name).reduce((acc, x) => acc.includes(x) ? acc : acc.concat(x), []);
151+
if (missingPackages.length || incorrectPackages.length) {
152+
console.log('Searching for solutions:');
153+
missingPackages.forEach(peer => findPossibleResolution(peer, peerDeps, true));
154+
incorrectPackages.forEach(peer => findPossibleResolution(peer, peerDeps, false));
155+
console.log();
156+
}
157+
158+
function getCommandLines() {
159+
const commands = [];
160+
if (yargs.argv.yarn) {
161+
if (adds.length) {
162+
commands.push(`yarn add ${adds.join(' ')}`);
163+
}
164+
if (upgrades.length) {
165+
commands.push(`yarn upgrade ${upgrades.join(' ')}`);
166+
}
167+
} else {
168+
commands.push(`npm install ${adds.concat(upgrades).join(' ')}`)
169+
}
170+
return commands;
171+
}
172+
173+
const commandLines = getCommandLines();
174+
if (commandLines.length) {
175+
console.log('Commands:');
176+
commandLines.forEach(command => {
177+
if (yargs.argv.install) {
178+
util._exec(command);
179+
} else {
180+
console.log(command);
181+
}
182+
});
183+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"bin": {
2020
"artifact_tagging": "./artifact_tagging.js",
21+
"check_peer_dependencies": "./check_peer_dependencies.js",
2122
"ensure_clean_master": "./ensure_clean_master.js",
2223
"generate_docs": "./generate_docs.js",
2324
"modify_sourcemap_paths": "./modify_sourcemap_paths.js",

0 commit comments

Comments
 (0)