UNPKG

installed-check-core

Version:

Checks that all dependencies in your package.json have supported versions installed

198 lines (160 loc) 6.69 kB
import { semverIntersect } from '@voxpelli/semver-set'; import { omit, pick, getObjectValueByPath, getStringValueByPath } from '@voxpelli/typed-utils'; import { validRange } from 'semver'; /** @typedef {{ valid: boolean|undefined, suggested?: string|undefined, note: string|undefined }} VersionRangeItem */ /** * @typedef VersionRangeOptions * @property {boolean|undefined} [expectedInDependencies=false] When set a warning will be issued when the key is empty or not found in a dependency * @property {boolean|undefined} [ignoreDefinitelyTyped=false] When set, all DefinitelyTyped packages will be ignored * @property {string[]|((test: string) => boolean)|undefined} [ignore] If set then the specified module names won't be included in the * @property {boolean|undefined} [noDev=false] If set then dev dependencies won't be included in the check * @property {boolean|undefined} [strict=false] Converts most warnings into failures */ /** @typedef {VersionRangeItem & { packageNotes: Array<VersionRangeItem & { name: string }> }} VersionRangeResult */ /** * @param {import('./lookup-types.d.ts').PackageJsonLike} pkg * @param {string} key * @param {import('./lookup-types.d.ts').InstalledDependencies} installed * @param {VersionRangeOptions} [options] * @returns {VersionRangeResult} */ export function checkVersionRange (pkg, key, installed, options) { const { ignore, ignoreDefinitelyTyped, noDev = false, strict = false, } = options || {}; const ignoreMatcher = ignore && !Array.isArray(ignore) ? ignore : undefined; const rawReferenceRange = getStringValueByPath(pkg, key); const invalidReferenceRange = rawReferenceRange && !validRange(rawReferenceRange); const referenceRange = (!rawReferenceRange || invalidReferenceRange) ? '*' : rawReferenceRange; /** @type {Array<VersionRangeItem & { name: string }>} */ const packageNotes = []; const requiredDependencies = omit({ ...pkg.dependencies, ...( noDev // Always include peer dependency dev deps ? pick(pkg.devDependencies || {}, Object.keys(pkg.peerDependencies || {})) : pkg.devDependencies ), }, Array.isArray(ignore) ? ignore : []); const optionalDependencies = { ...pkg.optionalDependencies }; /** @type {string | false} */ let intersection = referenceRange; for (const dependency in requiredDependencies) { if (ignoreMatcher && ignoreMatcher(dependency)) { continue; } if (ignoreDefinitelyTyped && dependency.startsWith('@types/')) { continue; } if (dependency === pkg.name) { continue; } const dependencyPackage = installed instanceof Map ? installed.get(dependency) : installed[dependency]; const isOptional = !!optionalDependencies[dependency]; const dependencyNotes = checkDependencyRange(referenceRange, key, dependencyPackage, isOptional, options); if (!dependencyNotes) { continue; } packageNotes.push({ name: dependency, ...dependencyNotes }); if (dependencyNotes.valid === false) { intersection = intersection && dependencyNotes.suggested ? semverIntersect(intersection, dependencyNotes.suggested) || false : false; } } const valid = intersection !== false && validRange(intersection) === validRange(referenceRange); /** @type {string|undefined} */ let note; if (rawReferenceRange === false) { note = `Invalid "${key}" in main package`; } else if (!rawReferenceRange) { note = `Missing "${key}" in main package`; } else if (invalidReferenceRange && !rawReferenceRange.startsWith('workspace:')) { note = `Invalid "${key}" range in main package: "${rawReferenceRange}"`; } return { valid: valid ? true : ((rawReferenceRange || strict) ? false : undefined), suggested: valid ? undefined : (intersection || undefined), packageNotes, note, }; } /** @typedef {VersionRangeOptions & { defaultKeys?: string[]|undefined }} VersionRangesOptions */ /** @typedef {VersionRangeResult & { topKey: string, childKey: string }} VersionRangeCollectionResult */ /** * @param {import('./lookup-types.d.ts').PackageJsonLike} pkg * @param {string} topKey * @param {import('./lookup-types.d.ts').InstalledDependencies} installed * @param {VersionRangesOptions} [options] * @returns {{ [key: string]: VersionRangeCollectionResult }} */ export function checkVersionRangeCollection (pkg, topKey, installed, options) { const { defaultKeys, ...restOptions } = options || {}; let foundKeys = Object.keys(getObjectValueByPath(pkg, topKey) || {}); if (foundKeys.length === 0 && defaultKeys) { foundKeys = defaultKeys; } /** @type {{ [key: string]: VersionRangeCollectionResult }} */ const result = {}; for (const childKey of foundKeys) { const key = `${topKey}.${childKey}`; result[key] = { ...checkVersionRange(pkg, key, installed, restOptions), topKey, childKey, }; } return result; } /** * @param {string} referenceRange * @param {string} key * @param {import('./lookup-types.d.ts').PackageJsonLike|undefined} dependencyPackage * @param {boolean} [isOptional] * @param {Omit<VersionRangeOptions, 'noDev'>} [options] * @returns {(VersionRangeItem & { valid: false | undefined }) | undefined} */ export function checkDependencyRange (referenceRange, key, dependencyPackage, isOptional, options) { const { expectedInDependencies = false, strict = false, } = options || {}; if (!dependencyPackage) { return { // Always a warning when optional, otherwise warning only when not strict valid: isOptional ? undefined : (strict ? false : undefined), note: 'Dependency is not installed. Can\'t check its requirements', }; } const range = getStringValueByPath(dependencyPackage, key); if (range === false) { return { valid: strict ? false : undefined, note: `Invalid "${key}"` }; } if (!range) { if (!expectedInDependencies) { return; } return strict ? { valid: false, note: `Required "${key}" is missing` } : { valid: undefined, note: `Missing "${key}"` }; } if (!validRange(range)) { if (range.startsWith('workspace:')) { return; } return { valid: strict ? false : undefined, note: `Invalid "${key}" range: "${range}"` }; } const intersection = semverIntersect(referenceRange, range); if (!intersection) { return { valid: false, note: `Incompatible "${key}" requirement: ${range}` }; } return validRange(intersection) === validRange(referenceRange) ? undefined : { valid: false, suggested: intersection, note: `Narrower "${key}" is needed: ${intersection}` }; }