Skip to content

Commit

Permalink
feat(peerDevDependencies): Add support for 'peerDevDependencies' -- '…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
christopherthielen committed Apr 10, 2020
1 parent a9c9fdf commit 47d40ef
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 27 deletions.
38 changes: 22 additions & 16 deletions src/checkPeerDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ import { exec } from 'shelljs';
import { CliOptions } from './cli';
import { getCommandLines } from './packageManager';
import { Dependency, gatherPeerDependencies, getInstalledVersion } from './packageUtils';
import { findPossibleResolutions } from './solution';
import { findPossibleResolutions, Resolution } from './solution';

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

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

return { ...dep, installedVersion, semverSatisfies, isYalc };
});
return allNestedPeerDependencies;
}

const allNestedPeerDependencies = gatheredDependencies.peerDependencies.map(applySemverInformation);
const allNestedPeerDevDependencies = gatheredDependencies.peerDevDependencies.map(applySemverInformation);

return { allNestedPeerDependencies, allNestedPeerDevDependencies };
}

let recursiveCount = 0;
Expand Down Expand Up @@ -51,16 +55,18 @@ const reportPeerDependencyStatusByDependee = (dep: Dependency, options: CliOptio
};

export function checkPeerDependencies(packageManager: string, options: CliOptions) {
const allNestedPeerDependencies = getAllNestedPeerDependencies(options);
const { allNestedPeerDependencies, allNestedPeerDevDependencies } = getAllNestedPeerDependencies(options);
const combinedPeerAndPeerDevDependencies = [...allNestedPeerDependencies, ...allNestedPeerDevDependencies];

if (options.orderBy === 'depender') {
allNestedPeerDependencies.sort((a, b) => `${a.depender}${a.name}`.localeCompare(`${b.depender}${b.name}`))
allNestedPeerDependencies.forEach(dep => reportPeerDependencyStatusByDepender(dep, options));
combinedPeerAndPeerDevDependencies.sort((a, b) => `${a.depender}${a.name}`.localeCompare(`${b.depender}${b.name}`));
combinedPeerAndPeerDevDependencies.forEach(dep => reportPeerDependencyStatusByDepender(dep, options));
} else if (options.orderBy === 'dependee') {
allNestedPeerDependencies.sort((a, b) => `${a.name}${a.depender}`.localeCompare(`${b.name}${b.depender}`))
allNestedPeerDependencies.forEach(dep => reportPeerDependencyStatusByDependee(dep, options));
combinedPeerAndPeerDevDependencies.sort((a, b) => `${a.name}${a.depender}`.localeCompare(`${b.name}${b.depender}`));
combinedPeerAndPeerDevDependencies.forEach(dep => reportPeerDependencyStatusByDependee(dep, options));
}

const problems = allNestedPeerDependencies.filter(dep => !dep.semverSatisfies && !dep.isYalc);
const problems = combinedPeerAndPeerDevDependencies.filter(dep => !dep.semverSatisfies && !dep.isYalc);

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

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

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

const newUnsatisfiedDeps = getAllNestedPeerDependencies(options)
const checkAgain = getAllNestedPeerDependencies(options);
const newUnsatisfiedDeps = [...checkAgain.allNestedPeerDependencies, ...checkAgain.allNestedPeerDevDependencies]
.filter(dep => !dep.semverSatisfies)
.filter(dep => !nosolution.some(x => isSameDep(x.problem, dep)));

Expand Down
4 changes: 2 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ const options = yarrrrgs
})
.option('yarn', {
boolean: true,
description: `Use yarn package manager`,
description: `Force yarn package manager`,
})
.option('npm', {
boolean: true,
description: `Use npm package manager`,
description: `Force npm package manager`,
})
.option('orderBy', {
choices: ['depender', 'dependee'],
Expand Down
17 changes: 15 additions & 2 deletions src/packageManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from "fs";
import { Resolution } from './solution';

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

export function getCommandLines(packageManager: string, installs: string[], upgrades: string[]) {
export function getCommandLines(packageManager: string, resolutions: Resolution[]) {
const installs = resolutions.filter(r => r.resolution && r.resolutionType === 'install').map(r => r.resolution);
const devInstalls = resolutions.filter(r => r.resolution && r.resolutionType === 'devInstall').map(r => r.resolution);
const upgrades = resolutions.filter(r => r.resolution && r.resolutionType === 'upgrade').map(r => r.resolution);

const commands = [];
if (packageManager === 'yarn') {
if (installs.length) {
commands.push(`yarn add ${installs.join(' ')}`);
}
if (devInstalls.length) {
commands.push(`yarn add -D ${devInstalls.join(' ')}`);
}
if (upgrades.length) {
commands.push(`yarn upgrade ${upgrades.join(' ')}`);
}
} else if (packageManager === 'npm' && (installs.length || upgrades.length)) {
commands.push(`npm install ${installs.concat(upgrades).join(' ')}`)
if (installs.length || upgrades.length) {
commands.push(`npm install ${installs.concat(upgrades).join(' ')}`);
}
if (devInstalls.length) {
commands.push(`npm install -D ${installs.concat(upgrades).join(' ')}`);
}
}
return commands;
}
28 changes: 25 additions & 3 deletions src/packageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ interface PackageJson {
peerDependencies: {
[key: string]: string;
};
// What is a peerDevDependency??!?! This is not a standard.
// It is only supported by this tool as a means to specify peerDependencies to install as devDependencies.
// This addresses a specific use case: to provide downstream projects with package building opinions such as
// a specific version of rollup and typescript.
peerDevDependencies: {
[key: string]: string;
};
}

export interface Dependency {
Expand All @@ -34,14 +41,22 @@ interface PackageDependencies {
dependencies: Dependency[];
devDependencies: Dependency[];
peerDependencies: Dependency[];
peerDevDependencies: Dependency[];
}

interface GatheredDependencies {
peerDependencies: Dependency[];
peerDevDependencies: Dependency[];
}

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

export function gatherPeerDependencies(packagePath, options: CliOptions): Dependency[] {
export function gatherPeerDependencies(packagePath, options: CliOptions): GatheredDependencies {
let peerDeps = [];
let peerDevDeps = [];
const visitor: DependencyWalkVisitor = (path, json, deps) => {
peerDeps = peerDeps.concat(deps.peerDependencies);
peerDevDeps = peerDevDeps.concat(deps.peerDevDependencies);
};
walkPackageDependencyTree(packagePath, visitor, [], options);

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

return peerDeps.reduce((acc: Dependency[], dep: Dependency) => {
const peerDependencies = peerDeps.reduce((acc: Dependency[], dep: Dependency) => {
return acc.some(dep2 => isSame(dep, dep2)) ? acc : acc.concat(dep);
}, [] as Dependency[])

const peerDevDependencies = peerDevDeps.reduce((acc: Dependency[], dep: Dependency) => {
return acc.some(dep2 => isSame(dep, dep2)) ? acc : acc.concat(dep);
}, [] as Dependency[])

return { peerDependencies, peerDevDependencies };
}

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

export function getPackageDependencies(packagePath: string, packageJson: PackageJson): PackageDependencies {
const { name, dependencies = {}, devDependencies = {}, peerDependencies = {} } = packageJson;
const { name, dependencies = {}, devDependencies = {}, peerDependencies = {}, peerDevDependencies = {} } = packageJson;

return {
packageName: name,
dependencies: buildDependencyArray(packagePath, packageJson, dependencies),
devDependencies: buildDependencyArray(packagePath, packageJson, devDependencies),
peerDependencies: buildDependencyArray(packagePath, packageJson, peerDependencies),
peerDevDependencies: buildDependencyArray(packagePath, packageJson, peerDevDependencies),
};
}

Expand Down
11 changes: 7 additions & 4 deletions src/solution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ function semverReverseSort(a, b) {
return -1;
}

interface Resolution {
export interface Resolution {
problem: Dependency;
resolution: string;
resolutionType: 'upgrade' | 'install';
resolutionType: 'upgrade' | 'install' | 'devInstall';
}

export function findPossibleResolutions(problems: Dependency[], allPeerDependencies: Dependency[]): Resolution[] {
export function findPossibleResolutions(problems: Dependency[], peerDependencies: Dependency[], peerDevDependencies: Dependency[]): Resolution[] {
const allPeerDependencies = [...peerDependencies, ...peerDevDependencies];
const uniq: Dependency[] = problems.reduce((acc, problem) => acc.some(dep => dep.name === problem.name) ? acc : acc.concat(problem), []);
return uniq.map(problem => {
const resolutionType = problem.installedVersion ? 'upgrade' : 'install';
const shouldUpgrade = !!problem.installedVersion;
const isPeerDevDep = peerDevDependencies.some(dep => dep.name === problem.name);
const resolutionType = shouldUpgrade ? 'upgrade' : isPeerDevDep ? 'devInstall' : 'install';
const resolutionVersion = findPossibleResolution(problem.name, allPeerDependencies);
const resolution = resolutionVersion ? `${problem.name}@${resolutionVersion}` : null;

Expand Down

0 comments on commit 47d40ef

Please sign in to comment.