Skip to content

Commit

Permalink
fix(peerDevDependencies): Use an array of package names in 'peerDevDe…
Browse files Browse the repository at this point in the history
…pendencies' in conjunction with the standard 'peerDependencies' object to install peer deps as devDependencies.

This now acts more as an extension of thstandard node packaging rather than adding completely custom behavior.

Also removed check-peer-dependencies-optional-dependency because its postinstall script only gets runs when check-peer-dependencies-optional-dependency package is re-installed.  So it was basically a failed experiment.
  • Loading branch information
christopherthielen committed Apr 10, 2020
1 parent 8035295 commit 681a80b
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 147 deletions.
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

### Features

* **check-peer-dependencies-optional-dependency:** Added a workaround for yarn swallowing postinstall script output ([a9c9fdf](https://github.com/christopherthielen/check-peer-dependencies/commit/a9c9fdf))
* **peerDevDependencies:** Add support for 'peerDevDependencies' -- 'peerDependencies' that should be installed as 'devDependencies' ([47d40ef](https://github.com/christopherthielen/check-peer-dependencies/commit/47d40ef))


Expand Down
48 changes: 20 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,41 @@ Options:

---

## Installing peerDependencies as devDependencies

If a package has a peerDependency that should be installed as a devDependency by,
it can list the package name in "peerDevDependencies".
This is not a standard and is only understood by this `check-peer-dependencies`.

```json
{
"name": "somepackage",
"peerDependencies": {
"react": "16.x",
"react-dom": "16.x",
"typescript": "~3.8.0",
"eslint": "*"
},
"peerDevDependencies": ["typescript", "eslint"]
}
```

## Example outputs:

### No problems

```bash
~/projects/uirouter/sample-app-react master
❯ npx check-peer-dependencies
✅ @uirouter/[email protected] requires @uirouter/core >=5.0.0 (5.0.23 is installed)
[email protected] requires ajv ^6.9.1 (6.10.2 is installed)
✅ @uirouter/[email protected] requires react ^16.3.0 (16.10.1 is installed)
[email protected] requires react ^16.0.0 (16.10.1 is installed)
[email protected] requires webpack ^2.0.0 || ^3.0.0 || ^4.0.0 (4.39.1 is installed)
No problems found!
✅ All peer dependencies are met
```

### Missing peer dependency, solution found

```bash
~/projects/uirouter/angular-hybrid master ⇣
❯ npx check-peer-dependencies
✅ @uirouter/[email protected] requires @angular/common ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 (5.2.11 is installed)
✅ @uirouter/[email protected] requires @angular/core ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 (5.2.11 is installed)
❌ @uirouter/[email protected] requires @angular/router ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 (@angular/router is not installed)
✅ @uirouter/[email protected] requires @angular/upgrade ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 (5.2.11 is installed)
✅ @uirouter/[email protected] requires @uirouter/core >=6.0.1 (6.0.1 is installed)
✅ @uirouter/[email protected] requires angular ^1.5.0 (1.7.8 is installed)
✅ @uirouter/[email protected] requires angular >=1.2.0 (1.7.8 is installed)
✅ @uirouter/[email protected] requires rxjs ^6.0.0 (6.5.3 is installed)

Searching for solutions:
yarn add @angular/[email protected]
Expand All @@ -63,21 +70,9 @@ yarn add @angular/[email protected]

```bash
❯ npx check-peer-dependencies
✅ @angular/[email protected] requires @angular/common 9.0.0-next.9 (9.0.0-next.9 is installed)
❌ @uirouter/[email protected] requires @angular/common ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 (9.0.0-next.9 is installed)
✅ @angular/[email protected] requires @angular/compiler 9.0.0-next.9 (9.0.0-next.9 is installed)
✅ @angular/[email protected] requires @angular/core 9.0.0-next.9 (9.0.0-next.9 is installed)
❌ @uirouter/[email protected] requires @angular/core ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 (9.0.0-next.9 is installed)
✅ @angular/[email protected] requires @angular/platform-browser 9.0.0-next.9 (9.0.0-next.9 is installed)
❌ @uirouter/[email protected] requires @angular/router ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 (9.0.0-next.9 is installed)
✅ @uirouter/[email protected] requires @uirouter/core >=6.0.1 (6.0.1 is installed)
✅ @uirouter/[email protected] requires @uirouter/core >=5.0.0 (6.0.1 is installed)
[email protected] requires ajv ^6.0.0 (6.10.2 is installed)
✅ @angular/[email protected] requires rxjs ^6.5.3 (6.5.3 is installed)
✅ @uirouter/[email protected] requires rxjs ^6.0.0 (6.5.3 is installed)
[email protected] requires typescript >=1.8.0 <2.1.0 || >=1.9.0-dev || >=2.0.0-dev || || >=2.1.0-dev (3.5.3 is installed)
[email protected] requires webpack ^2.0.0 || ^3.0.0 || ^4.0.0 (4.41.0 is installed)
✅ @angular/[email protected] requires zone.js ~0.10.2 (0.10.2 is installed)

Searching for solutions:
❌ Unable to find a version of @angular/common that satisfies the following peerDependencies: 9.0.0-next.9 and ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
Expand All @@ -86,6 +81,3 @@ Searching for solutions:
yarn upgrade @angular/[email protected]
```

## Running as postinstall script using `yarn` package manager

Please see [check-peer-dependencies-optional-dependency](https://github.com/christopherthielen/check-peer-dependencies/tree/master/packages/check-peer-dependencies-optional-dependency)
36 changes: 0 additions & 36 deletions packages/check-peer-dependencies-optional-dependency/README.md

This file was deleted.

10 changes: 0 additions & 10 deletions packages/check-peer-dependencies-optional-dependency/package.json

This file was deleted.

45 changes: 12 additions & 33 deletions src/checkPeerDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import * as semver from 'semver';
import { exec } from 'shelljs';
import { CliOptions } from './cli';
import { getCommandLines } from './packageManager';
import { Dependency, gatherPeerDependencies, getInstalledVersion } from './packageUtils';
import { Dependency, gatherPeerDependencies, getInstalledVersion, isSameDep } from './packageUtils';
import { findPossibleResolutions, Resolution } from './solution';

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

function applySemverInformation(dep: Dependency): Dependency {
Expand All @@ -18,10 +18,7 @@ function getAllNestedPeerDependencies(options: CliOptions) {
return { ...dep, installedVersion, semverSatisfies, isYalc };
}

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

return { allNestedPeerDependencies, allNestedPeerDevDependencies };
return gatheredDependencies.map(applySemverInformation);
}

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

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

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

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

if (!problems.length) {
console.log(' ✅ All peer dependencies are met');
Expand All @@ -76,14 +72,14 @@ export function checkPeerDependencies(packageManager: string, options: CliOption
console.log();
console.log('Searching for solutions...');
console.log();
const resolutions: Resolution[] = findPossibleResolutions(problems, allNestedPeerDependencies, allNestedPeerDevDependencies);
const resolutions: Resolution[] = findPossibleResolutions(problems, allNestedPeerDependencies);
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 = combinedPeerAndPeerDevDependencies.filter(dep => dep.name === name)
const peerDepRanges = allNestedPeerDependencies.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 @@ -103,8 +99,7 @@ export function checkPeerDependencies(packageManager: string, options: CliOption
console.log();
});

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

Expand All @@ -131,19 +126,3 @@ export function checkPeerDependencies(packageManager: string, options: CliOption
}
process.exit(1);
}


function isSameDep(a: Dependency, b: Dependency) {
const keys: Array<keyof Dependency> = [
"name",
"version",
"depender",
"dependerPath",
"dependerVersion",
"installedVersion",
"semverSatisfies",
"isYalc"
];

return keys.every(key => a[key] === b[key]);
}
72 changes: 37 additions & 35 deletions src/packageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ 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.

// What is a peerDevDependency? This is not a standard.
// This is an array of package names found in `peerDependencies` which should be installed 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;
};
// a specific version of react and rollup and typescript.
// Example:
// peerDevDependencies: ["rollup", "typescript"]
peerDevDependencies: string[];
}

export interface Dependency {
Expand All @@ -34,49 +35,30 @@ export interface Dependency {
installedVersion?: string | undefined;
semverSatisfies?: boolean;
isYalc?: boolean;
isPeerDevDependency?: boolean;
}

interface PackageDependencies {
packageName: string;
dependencies: Dependency[];
devDependencies: Dependency[];
peerDependencies: Dependency[];
peerDevDependencies: Dependency[];
}

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

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

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

// Eliminate duplicates
const isSame = (dep: Dependency, dep2: Dependency) => {
return dep.name === dep2.name
&& dep.version === dep2.version
&& dep.depender === dep2.depender
&& dep.dependerVersion === dep2.dependerVersion;
};

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 };
return peerDeps.reduce((acc: Dependency[], dep: Dependency) => {
return acc.some(dep2 => isSameDep(dep, dep2)) ? acc : acc.concat(dep);
}, [] as Dependency[]);
}

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

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

const applyPeerDevDependencies= (dep: Dependency): Dependency =>
({ ...dep, isPeerDevDependency: peerDevDependencies.includes(dep.name) });

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

Expand Down Expand Up @@ -168,3 +153,20 @@ export function getInstalledVersion(dep: Dependency): string | undefined {
const isYalc = fs.existsSync(path.resolve(peerDependencyDir, 'yalc.sig'));
return isYalc ? `${packageJson.version}-yalc` : packageJson.version;
}


export function isSameDep(a: Dependency, b: Dependency) {
const keys: Array<keyof Dependency> = [
"name",
"version",
"depender",
"dependerPath",
"dependerVersion",
"installedVersion",
"semverSatisfies",
"isYalc",
"isPeerDevDependency",
];

return keys.every(key => a[key] === b[key]);
}
6 changes: 2 additions & 4 deletions src/solution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ export interface Resolution {
resolutionType: 'upgrade' | 'install' | 'devInstall';
}

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

Expand Down

0 comments on commit 681a80b

Please sign in to comment.