Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate peer dependency ranges #117

Merged
merged 1 commit into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ if (result.valid === true) {
}
```

### checkVersionRangeCollection()


Wrapper around as [`checkVersionRange()`](#checkversionrange) that differs from it in three ways:

* `key` is for a collection of range, eg `engines` rather than `engines.node`
* The results for every individual version range is returned in an `object` keyed with the full key for that range, eg: `{ 'engines.node': ... }`
* Accepts an additional optional `defaultKeys` option that's used if the collection for `key` is empty. Eg: `{ defaultKeys: ['node'] }`

#### Syntax

```ts
checkVersionRangeCollection(mainPackage, key, installedDependencies, [options]) => VersionRangesResult
```

#### Arguments

See main description of [`checkVersionRangeCollection()`](#checkversionrangecollection) and full docs for [`checkVersionRange()`](#checkversionrange).

### getInstalledData()

Companion method to eg. `checkVersionRange()` that which makes it easy to get the correct data required. Not meant for any other use.
Expand Down Expand Up @@ -165,25 +184,27 @@ installedCheck(checks, options) => Promise<InstalledCheckResult>

#### Arguments

* `checks`: Type `('engine' | 'version')[]` – the checks to run
* `checks`: Type `InstalledChecks[]` – the checks to run, an array of one or more of: `'engine'`, `'peer'`, `'version'`
* `options`: Type `InstalledCheckOptions`

#### Types

```ts
type InstalledChecks = 'engine' | 'peer' | 'version'
type InstalledCheckResult = { errors: string[], warnings: string[] }
```

#### Checks

* `engine` – will check that the installed modules comply with the [engines requirements](https://docs.npmjs.com/files/package.json#engines) of the `package.json` and suggest an alternative requirement if the installed modules don't comply.
* `peer` – like `engine` but for `peerDependencies` instead. Will check that the promised `peerDependencies` are not wider than those of ones required dependencies.
* `version` – will check that the installed modules comply with the version requirements set for them the `package.json`.

#### Options

* `path = '.'` – specifies the path to the package to be checked, with its `package.json` expected to be there and its installed `node_modules` as well.
* `ignores = string[]` – names of modules to exclude from checks. Supports [`picomatch`](https://www.npmjs.com/package/picomatch) globbing syntax, eg. `@types/*`. (Not supported by `version` checks)
* `noDev = false` – exclude dev dependencies from checks (Not supported by `version` checks)
* `noDev = false` – exclude `devDependencies` from checks. `devDependencies` that are also in `peerDependencies` will not be ignored. (Not supported by `version` checks)
* `strict = false` – converts most warnings into failures

#### Example
Expand All @@ -210,7 +231,7 @@ performInstalledCheck(checks, mainPackage, installedDependencies, options) => Pr

#### Arguments

* `checks`: Type `('engine' | 'version')[]` – same as for [`installedCheck()`](#installedcheck)
* `checks`: Type `InstalledChecks[]` – same as for [`installedCheck()`](#installedcheck)
* `mainPackage`: Type `PackageJsonLike` – the content of the `package.json` file to check, see [`getInstalledData()`](#getinstalleddata)
* `installedDependencies`: Type `InstalledDependencies` – the installed dependencies to use when checking, see [`getInstalledData()`](#getinstalleddata)
* `options`: Type `InstalledCheckOptions` – same as for [`installedCheck()`](#installedcheck), but without the `path` option
Expand Down
6 changes: 5 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/** @typedef {import('./lib/check-version-range.js').VersionRangeItem} VersionRangeItem */
/** @typedef {import('./lib/check-version-range.js').VersionRangeOptions} VersionRangeOptions */
/** @typedef {import('./lib/check-version-range.js').VersionRangeResult} VersionRangeResult */
/** @typedef {import('./lib/check-version-range.js').VersionRangeOptions} VersionRangesOptions */

/** @typedef {import('./lib/get-installed-data.js').PackageJsonLike} PackageJsonLike */
/** @typedef {import('./lib/get-installed-data.js').InstalledDependencies} InstalledDependencies */

/** @typedef {import('./lib/perform-installed-check.js').InstalledChecks} InstalledChecks */
/** @typedef {import('./lib/perform-installed-check.js').InstalledCheckOptions} InstalledCheckOptions */
/** @typedef {import('./lib/perform-installed-check.js').InstalledCheckResult} InstalledCheckResult */

export { checkVersionRange } from './lib/check-version-range.js';
export { checkVersionRange, checkVersionRangeCollection } from './lib/check-version-range.js';
export { getInstalledData } from './lib/get-installed-data.js';
export { installedCheck } from './lib/installed-check.js';
export { performInstalledCheck } from './lib/perform-installed-check.js';
54 changes: 0 additions & 54 deletions lib/check-engine-versions.js

This file was deleted.

42 changes: 39 additions & 3 deletions lib/check-version-range.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { semverIntersect } from '@voxpelli/semver-set';
import { omit } from '@voxpelli/typed-utils';
import { omit, pick } from '@voxpelli/typed-utils';
import { validRange } from 'semver';

import { getStringValueByPath } from './utils.js';
import { getObjectValueByPath, getStringValueByPath } from './utils.js';

/** @typedef {{ valid: boolean|undefined, suggested?: string|undefined, note: string|undefined }} VersionRangeItem */

Expand Down Expand Up @@ -40,8 +40,14 @@ export function checkVersionRange (mainPackage, key, installedDependencies, opti

const requiredDependencies = omit({
...mainPackage.dependencies,
...(noDev ? mainPackage.devDependencies : {}),
...(
noDev
// Always include peer dependency dev deps
? pick(mainPackage.devDependencies || {}, Object.keys(mainPackage.peerDependencies || {}))
: mainPackage.devDependencies
),
}, Array.isArray(ignore) ? ignore : []);

const optionalDependencies = { ...mainPackage.optionalDependencies };

/** @type {string | false} */
Expand Down Expand Up @@ -84,6 +90,36 @@ export function checkVersionRange (mainPackage, key, installedDependencies, opti
};
}

/** @typedef {VersionRangeOptions & { defaultKeys?: string[] }} VersionRangesOptions */

/**
* @param {import('./get-installed-data.js').PackageJsonLike} mainPackage
* @param {string} topKey
* @param {import('./get-installed-data.js').InstalledDependencies} installedDependencies
* @param {VersionRangesOptions} [options]
* @returns {{ [key: string]: VersionRangeResult }}
*/
export function checkVersionRangeCollection (mainPackage, topKey, installedDependencies, options) {
const { defaultKeys, ...restOptions } = options || {};

let foundKeys = Object.keys(getObjectValueByPath(mainPackage, topKey) || {});

if (foundKeys.length === 0 && defaultKeys) {
foundKeys = defaultKeys;
}

/** @type {{ [key: string]: VersionRangeResult }} */
const result = {};

for (const childKey of foundKeys) {
const key = `${topKey}.${childKey}`;

result[key] = checkVersionRange(mainPackage, key, installedDependencies, restOptions);
}

return result;
}

/**
* @param {string} referenceRange
* @param {string} key
Expand Down
40 changes: 40 additions & 0 deletions lib/check-versions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { checkVersionRangeCollection } from './check-version-range.js';

/**
* @param {import('./get-installed-data.js').PackageJsonLike} mainPackage
* @param {string} key
* @param {import('./get-installed-data.js').InstalledDependencies} installedDependencies
* @param {import('./check-version-range.js').VersionRangesOptions} options
* @returns {{ errors: string[], warnings: string[] }}
*/
export function checkVersions (mainPackage, key, installedDependencies, options) {
/** @type {string[]} */
const errors = [];
/** @type {string[]} */
const warnings = [];
/** @type {string[]} */
const summaries = [];

const rangesResult = checkVersionRangeCollection(mainPackage, key, installedDependencies, options);

for (const [name, rangeResult] of Object.entries(rangesResult)) {
for (const result of [rangeResult, ...rangeResult.packageNotes]) {
if (result.note) {
(result.valid === false ? errors : warnings).push(('name' in result ? `${result.name}: ` : '') + result.note);
}
}

if (!rangeResult.valid) {
summaries.push((
rangeResult.suggested
? `Combined "${name}" needs to be narrower: ${rangeResult.suggested}`
: `Incompatible combined "${name}" requirements.`
));
}
}

return {
errors: [...new Set(errors), ...summaries],
warnings: [...new Set(warnings)],
};
}
12 changes: 9 additions & 3 deletions lib/perform-installed-check.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { explainVariable, typedObjectKeys } from '@voxpelli/typed-utils';
import isGlob from 'is-glob';

import { checkEngineVersions } from './check-engine-versions.js';
import { checkPackageVersions } from './check-package-versions.js';
import { checkVersions } from './check-versions.js';

/**
* @typedef InstalledCheckResult
* @property {string[]} errors
* @property {string[]} warnings
*/

/** @typedef {'engine' | 'version'} InstalledChecks */
/** @typedef {'engine' | 'peer' | 'version'} InstalledChecks */

/** @type {Record<InstalledChecks, true>} */
const checkTypeMap = {
'engine': true,
'peer': true,
'version': true,
};

Expand Down Expand Up @@ -68,7 +69,12 @@ export async function performInstalledCheck (checks, mainPackage, installedDepen

const results = [
checks.includes('version') && checkPackageVersions(mainPackage, installedDependencies),
checks.includes('engine') && checkEngineVersions(mainPackage, installedDependencies, checkOptions),
checks.includes('engine') && checkVersions(mainPackage, 'engines', installedDependencies, {
...checkOptions,
defaultKeys: ['node'],
expectedInDependencies: true,
}),
checks.includes('peer') && checkVersions(mainPackage, 'peerDependencies', installedDependencies, checkOptions),
];

for (const result of results) {
Expand Down
21 changes: 21 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,24 @@ export function getStringValueByPath (obj, path) {
? undefined
: (typeof currentValue === 'string' ? currentValue : false);
}

/**
* @param {unknown} obj
* @param {string} path
* @returns {Record<string, unknown>|undefined|false}
*/
export function getObjectValueByPath (obj, path) {
let currentValue = obj;

for (const key of path.split('.')) {
if (!currentValue || typeof currentValue !== 'object') {
return;
}

currentValue = currentValue[/** @type {keyof typeof currentValue} */ (key)];
}

return currentValue === undefined
? undefined
: (currentValue && typeof currentValue === 'object' ? { ...currentValue } : false);
}
5 changes: 5 additions & 0 deletions test/fixtures/peer/node_modules/bar/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions test/fixtures/peer/node_modules/foo/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions test/fixtures/peer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"peerDependencies": {
"bar": ">=4.0.0"
},
"devDependencies": {
"bar": "^5.0.0",
"foo": "^1.0.0"
}
}
14 changes: 14 additions & 0 deletions test/installed-check.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,19 @@ describe('installedCheck()', () => {
],
});
});

it('should check peer dependencies', async () => {
await installedCheck(['peer'], {
path: join(import.meta.url, 'fixtures/peer'),
})
.should.eventually.deep.equal({
'errors': [
'foo: Narrower "peerDependencies.bar" is needed: >=4.6.8',
'Combined "peerDependencies.bar" needs to be narrower: >=4.6.8',
],
warnings: [
],
});
});
});
});
Loading