Skip to content

Commit 47d40ef

Browse files
feat(peerDevDependencies): Add support for 'peerDevDependencies' -- 'peerDependencies' that should be installed as 'devDependencies'
This is a feature that I want in order to have downstream projects install a specific set of dev dependencies. This will allow a meta-package (similar to react-scripts) to suggest a specific set of devDependencies that are installed as top-level devDependencies
1 parent a9c9fdf commit 47d40ef

File tree

5 files changed

+71
-27
lines changed

5 files changed

+71
-27
lines changed

src/checkPeerDependencies.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@ import { exec } from 'shelljs';
55
import { CliOptions } from './cli';
66
import { getCommandLines } from './packageManager';
77
import { Dependency, gatherPeerDependencies, getInstalledVersion } from './packageUtils';
8-
import { findPossibleResolutions } from './solution';
8+
import { findPossibleResolutions, Resolution } from './solution';
99

1010
function getAllNestedPeerDependencies(options: CliOptions) {
1111
const gatheredDependencies = gatherPeerDependencies(".", options);
1212

13-
const allNestedPeerDependencies: Dependency[] = gatheredDependencies.map(dep => {
13+
function applySemverInformation(dep: Dependency): Dependency {
1414
const installedVersion = getInstalledVersion(dep);
1515
const semverSatisfies = installedVersion ? semver.satisfies(installedVersion, dep.version) : false;
1616
const isYalc = !!/-[a-f0-9]+-yalc$/.exec(installedVersion);
1717

1818
return { ...dep, installedVersion, semverSatisfies, isYalc };
19-
});
20-
return allNestedPeerDependencies;
19+
}
20+
21+
const allNestedPeerDependencies = gatheredDependencies.peerDependencies.map(applySemverInformation);
22+
const allNestedPeerDevDependencies = gatheredDependencies.peerDevDependencies.map(applySemverInformation);
23+
24+
return { allNestedPeerDependencies, allNestedPeerDevDependencies };
2125
}
2226

2327
let recursiveCount = 0;
@@ -51,16 +55,18 @@ const reportPeerDependencyStatusByDependee = (dep: Dependency, options: CliOptio
5155
};
5256

5357
export function checkPeerDependencies(packageManager: string, options: CliOptions) {
54-
const allNestedPeerDependencies = getAllNestedPeerDependencies(options);
58+
const { allNestedPeerDependencies, allNestedPeerDevDependencies } = getAllNestedPeerDependencies(options);
59+
const combinedPeerAndPeerDevDependencies = [...allNestedPeerDependencies, ...allNestedPeerDevDependencies];
60+
5561
if (options.orderBy === 'depender') {
56-
allNestedPeerDependencies.sort((a, b) => `${a.depender}${a.name}`.localeCompare(`${b.depender}${b.name}`))
57-
allNestedPeerDependencies.forEach(dep => reportPeerDependencyStatusByDepender(dep, options));
62+
combinedPeerAndPeerDevDependencies.sort((a, b) => `${a.depender}${a.name}`.localeCompare(`${b.depender}${b.name}`));
63+
combinedPeerAndPeerDevDependencies.forEach(dep => reportPeerDependencyStatusByDepender(dep, options));
5864
} else if (options.orderBy === 'dependee') {
59-
allNestedPeerDependencies.sort((a, b) => `${a.name}${a.depender}`.localeCompare(`${b.name}${b.depender}`))
60-
allNestedPeerDependencies.forEach(dep => reportPeerDependencyStatusByDependee(dep, options));
65+
combinedPeerAndPeerDevDependencies.sort((a, b) => `${a.name}${a.depender}`.localeCompare(`${b.name}${b.depender}`));
66+
combinedPeerAndPeerDevDependencies.forEach(dep => reportPeerDependencyStatusByDependee(dep, options));
6167
}
6268

63-
const problems = allNestedPeerDependencies.filter(dep => !dep.semverSatisfies && !dep.isYalc);
69+
const problems = combinedPeerAndPeerDevDependencies.filter(dep => !dep.semverSatisfies && !dep.isYalc);
6470

6571
if (!problems.length) {
6672
console.log(' ✅ All peer dependencies are met');
@@ -70,15 +76,14 @@ export function checkPeerDependencies(packageManager: string, options: CliOption
7076
console.log();
7177
console.log('Searching for solutions...');
7278
console.log();
73-
const resolutions = findPossibleResolutions(problems, allNestedPeerDependencies);
74-
const installs = resolutions.filter(r => r.resolution && r.resolutionType === 'install').map(r => r.resolution);
75-
const upgrades = resolutions.filter(r => r.resolution && r.resolutionType === 'upgrade').map(r => r.resolution);
79+
const resolutions: Resolution[] = findPossibleResolutions(problems, allNestedPeerDependencies, allNestedPeerDevDependencies);
80+
const resolutionsWithSolutions = resolutions.filter(r => r.resolution);
7681
const nosolution = resolutions.filter(r => !r.resolution);
7782

7883
nosolution.forEach(solution => {
7984
const name = solution.problem.name;
8085
const errorPrefix = `Unable to find a version of ${name} that satisfies the following peerDependencies:`;
81-
const peerDepRanges = allNestedPeerDependencies.filter(dep => dep.name === name)
86+
const peerDepRanges = combinedPeerAndPeerDevDependencies.filter(dep => dep.name === name)
8287
.reduce((acc, dep) => acc.includes(dep.version) ? acc : acc.concat(dep.version), []);
8388
console.error(` ❌ ${errorPrefix} ${peerDepRanges.join(" and ")}`)
8489
});
@@ -88,7 +93,7 @@ export function checkPeerDependencies(packageManager: string, options: CliOption
8893
console.error();
8994
}
9095

91-
const commandLines = getCommandLines(packageManager, installs, upgrades);
96+
const commandLines = getCommandLines(packageManager, resolutionsWithSolutions);
9297
if (options.install && commandLines.length > 0) {
9398
console.log('Installing peerDependencies...');
9499
console.log();
@@ -98,7 +103,8 @@ export function checkPeerDependencies(packageManager: string, options: CliOption
98103
console.log();
99104
});
100105

101-
const newUnsatisfiedDeps = getAllNestedPeerDependencies(options)
106+
const checkAgain = getAllNestedPeerDependencies(options);
107+
const newUnsatisfiedDeps = [...checkAgain.allNestedPeerDependencies, ...checkAgain.allNestedPeerDevDependencies]
102108
.filter(dep => !dep.semverSatisfies)
103109
.filter(dep => !nosolution.some(x => isSameDep(x.problem, dep)));
104110

src/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ const options = yarrrrgs
1212
})
1313
.option('yarn', {
1414
boolean: true,
15-
description: `Use yarn package manager`,
15+
description: `Force yarn package manager`,
1616
})
1717
.option('npm', {
1818
boolean: true,
19-
description: `Use npm package manager`,
19+
description: `Force npm package manager`,
2020
})
2121
.option('orderBy', {
2222
choices: ['depender', 'dependee'],

src/packageManager.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as fs from "fs";
2+
import { Resolution } from './solution';
23

34
export function getPackageManager(forceYarn: boolean, forceNpm: boolean) {
45
if (forceYarn) return 'yarn';
@@ -7,17 +8,29 @@ export function getPackageManager(forceYarn: boolean, forceNpm: boolean) {
78
if (fs.existsSync('package-lock.json')) return 'npm';
89
}
910

10-
export function getCommandLines(packageManager: string, installs: string[], upgrades: string[]) {
11+
export function getCommandLines(packageManager: string, resolutions: Resolution[]) {
12+
const installs = resolutions.filter(r => r.resolution && r.resolutionType === 'install').map(r => r.resolution);
13+
const devInstalls = resolutions.filter(r => r.resolution && r.resolutionType === 'devInstall').map(r => r.resolution);
14+
const upgrades = resolutions.filter(r => r.resolution && r.resolutionType === 'upgrade').map(r => r.resolution);
15+
1116
const commands = [];
1217
if (packageManager === 'yarn') {
1318
if (installs.length) {
1419
commands.push(`yarn add ${installs.join(' ')}`);
1520
}
21+
if (devInstalls.length) {
22+
commands.push(`yarn add -D ${devInstalls.join(' ')}`);
23+
}
1624
if (upgrades.length) {
1725
commands.push(`yarn upgrade ${upgrades.join(' ')}`);
1826
}
1927
} else if (packageManager === 'npm' && (installs.length || upgrades.length)) {
20-
commands.push(`npm install ${installs.concat(upgrades).join(' ')}`)
28+
if (installs.length || upgrades.length) {
29+
commands.push(`npm install ${installs.concat(upgrades).join(' ')}`);
30+
}
31+
if (devInstalls.length) {
32+
commands.push(`npm install -D ${installs.concat(upgrades).join(' ')}`);
33+
}
2134
}
2235
return commands;
2336
}

src/packageUtils.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ interface PackageJson {
1616
peerDependencies: {
1717
[key: string]: string;
1818
};
19+
// What is a peerDevDependency??!?! This is not a standard.
20+
// It is only supported by this tool as a means to specify peerDependencies to install as devDependencies.
21+
// This addresses a specific use case: to provide downstream projects with package building opinions such as
22+
// a specific version of rollup and typescript.
23+
peerDevDependencies: {
24+
[key: string]: string;
25+
};
1926
}
2027

2128
export interface Dependency {
@@ -34,14 +41,22 @@ interface PackageDependencies {
3441
dependencies: Dependency[];
3542
devDependencies: Dependency[];
3643
peerDependencies: Dependency[];
44+
peerDevDependencies: Dependency[];
45+
}
46+
47+
interface GatheredDependencies {
48+
peerDependencies: Dependency[];
49+
peerDevDependencies: Dependency[];
3750
}
3851

3952
type DependencyWalkVisitor = (packagePath: string, packageJson: PackageJson, packageDependencies: PackageDependencies) => void;
4053

41-
export function gatherPeerDependencies(packagePath, options: CliOptions): Dependency[] {
54+
export function gatherPeerDependencies(packagePath, options: CliOptions): GatheredDependencies {
4255
let peerDeps = [];
56+
let peerDevDeps = [];
4357
const visitor: DependencyWalkVisitor = (path, json, deps) => {
4458
peerDeps = peerDeps.concat(deps.peerDependencies);
59+
peerDevDeps = peerDevDeps.concat(deps.peerDevDependencies);
4560
};
4661
walkPackageDependencyTree(packagePath, visitor, [], options);
4762

@@ -53,9 +68,15 @@ export function gatherPeerDependencies(packagePath, options: CliOptions): Depend
5368
&& dep.dependerVersion === dep2.dependerVersion;
5469
};
5570

56-
return peerDeps.reduce((acc: Dependency[], dep: Dependency) => {
71+
const peerDependencies = peerDeps.reduce((acc: Dependency[], dep: Dependency) => {
5772
return acc.some(dep2 => isSame(dep, dep2)) ? acc : acc.concat(dep);
5873
}, [] as Dependency[])
74+
75+
const peerDevDependencies = peerDevDeps.reduce((acc: Dependency[], dep: Dependency) => {
76+
return acc.some(dep2 => isSame(dep, dep2)) ? acc : acc.concat(dep);
77+
}, [] as Dependency[])
78+
79+
return { peerDependencies, peerDevDependencies };
5980
}
6081

6182
export function walkPackageDependencyTree(packagePath: string, visitor: DependencyWalkVisitor, visitedPaths: string[], options: CliOptions) {
@@ -105,13 +126,14 @@ function buildDependencyArray(packagePath: string, packageJson: PackageJson, dep
105126
}
106127

107128
export function getPackageDependencies(packagePath: string, packageJson: PackageJson): PackageDependencies {
108-
const { name, dependencies = {}, devDependencies = {}, peerDependencies = {} } = packageJson;
129+
const { name, dependencies = {}, devDependencies = {}, peerDependencies = {}, peerDevDependencies = {} } = packageJson;
109130

110131
return {
111132
packageName: name,
112133
dependencies: buildDependencyArray(packagePath, packageJson, dependencies),
113134
devDependencies: buildDependencyArray(packagePath, packageJson, devDependencies),
114135
peerDependencies: buildDependencyArray(packagePath, packageJson, peerDependencies),
136+
peerDevDependencies: buildDependencyArray(packagePath, packageJson, peerDevDependencies),
115137
};
116138
}
117139

src/solution.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ function semverReverseSort(a, b) {
1313
return -1;
1414
}
1515

16-
interface Resolution {
16+
export interface Resolution {
1717
problem: Dependency;
1818
resolution: string;
19-
resolutionType: 'upgrade' | 'install';
19+
resolutionType: 'upgrade' | 'install' | 'devInstall';
2020
}
2121

22-
export function findPossibleResolutions(problems: Dependency[], allPeerDependencies: Dependency[]): Resolution[] {
22+
export function findPossibleResolutions(problems: Dependency[], peerDependencies: Dependency[], peerDevDependencies: Dependency[]): Resolution[] {
23+
const allPeerDependencies = [...peerDependencies, ...peerDevDependencies];
2324
const uniq: Dependency[] = problems.reduce((acc, problem) => acc.some(dep => dep.name === problem.name) ? acc : acc.concat(problem), []);
2425
return uniq.map(problem => {
25-
const resolutionType = problem.installedVersion ? 'upgrade' : 'install';
26+
const shouldUpgrade = !!problem.installedVersion;
27+
const isPeerDevDep = peerDevDependencies.some(dep => dep.name === problem.name);
28+
const resolutionType = shouldUpgrade ? 'upgrade' : isPeerDevDep ? 'devInstall' : 'install';
2629
const resolutionVersion = findPossibleResolution(problem.name, allPeerDependencies);
2730
const resolution = resolutionVersion ? `${problem.name}@${resolutionVersion}` : null;
2831

0 commit comments

Comments
 (0)