UNPKG

check-dependency-version-consistency

Version:

Ensures dependencies are on consistent versions across a monorepo.

248 lines (247 loc) 13.1 kB
import editJsonFile from 'edit-json-file'; import { Package } from './package.js'; import { compareVersionRanges, compareVersionRangesSafe, versionRangeToRange, getIncreasedLatestVersion, getHighestRangeType, } from './semver.js'; import semver from 'semver'; import { DEPENDENCY_TYPE } from './types.js'; import { DEFAULT_DEP_TYPES } from './defaults.js'; /** * Creates a map of each dependency in the workspace to an array of the packages it is used in. * * Example of such a map represented as an object: * * { * 'ember-cli': [ * { package: Package...'@scope/package1', version: '~3.18.0' }, * { package: Package...'@scope/package2', version: '~3.18.0' } * ] * 'eslint': [ * { package: Package...'@scope/package1', version: '^7.0.0' }, * { package: Package...'@scope/package2', version: '^7.0.0' } * ] * } */ export function calculateVersionsForEachDependency(packages, depType = DEFAULT_DEP_TYPES) { const dependenciesToVersionsSeen = new Map(); for (const package_ of packages) { recordDependencyVersionsForPackageJson(dependenciesToVersionsSeen, package_, depType); } return dependenciesToVersionsSeen; } // eslint-disable-next-line complexity function recordDependencyVersionsForPackageJson(dependenciesToVersionsSeen, package_, depType) { if (package_.packageJson.name && package_.packageJson.version) { recordDependencyVersion(dependenciesToVersionsSeen, package_.packageJson.name, package_.packageJson.version, package_, true); } if (depType.includes(DEPENDENCY_TYPE.dependencies) && package_.packageJson.dependencies) { for (const [dependency, dependencyVersion] of Object.entries(package_.packageJson.dependencies)) { if (dependencyVersion) { recordDependencyVersion(dependenciesToVersionsSeen, dependency, dependencyVersion, package_); } } } if (depType.includes(DEPENDENCY_TYPE.devDependencies) && package_.packageJson.devDependencies) { for (const [dependency, dependencyVersion] of Object.entries(package_.packageJson.devDependencies)) { if (dependencyVersion) { recordDependencyVersion(dependenciesToVersionsSeen, dependency, dependencyVersion, package_); } } } if (depType.includes(DEPENDENCY_TYPE.optionalDependencies) && package_.packageJson.optionalDependencies) { for (const [dependency, dependencyVersion] of Object.entries(package_.packageJson.optionalDependencies)) { if (dependencyVersion) { recordDependencyVersion(dependenciesToVersionsSeen, dependency, dependencyVersion, package_); } } } if (depType.includes(DEPENDENCY_TYPE.peerDependencies) && package_.packageJson.peerDependencies) { for (const [dependency, dependencyVersion] of Object.entries(package_.packageJson.peerDependencies)) { if (dependencyVersion) { recordDependencyVersion(dependenciesToVersionsSeen, dependency, dependencyVersion, package_); } } } if (depType.includes(DEPENDENCY_TYPE.resolutions) && package_.packageJson.resolutions) { for (const [dependency, dependencyVersion] of Object.entries(package_.packageJson.resolutions)) { if (dependencyVersion) { recordDependencyVersion(dependenciesToVersionsSeen, dependency, dependencyVersion, package_); } } } } function recordDependencyVersion(dependenciesToVersionsSeen, dependency, version, package_, isLocalPackageVersion = false) { if (!dependenciesToVersionsSeen.has(dependency)) { dependenciesToVersionsSeen.set(dependency, []); } const list = dependenciesToVersionsSeen.get(dependency); /* istanbul ignore if */ if (list) { // `list` should always exist at this point, this if statement is just to please TypeScript. list.push({ package: package_, version, isLocalPackageVersion }); } } export function calculateDependenciesAndVersions(dependencyVersions) { // Loop through all dependencies seen. return [...dependencyVersions.entries()] .sort((a, b) => a[0].localeCompare(b[0])) .flatMap(([dependency, versionObjectsForDep]) => { // Check what versions we have seen for this dependency. let versions = versionObjectsForDep .filter((versionObject) => !versionObject.isLocalPackageVersion) .map((versionObject) => versionObject.version); // Check if this dependency is a local package. const localPackageVersions = versionObjectsForDep .filter((versionObject) => versionObject.isLocalPackageVersion) .map((versionObject) => versionObject.version); const allVersionsHaveWorkspacePrefix = versions.every((version) => version.startsWith('workspace:')); const hasIncompatibilityWithLocalPackageVersion = versions.some((version) => !semver.satisfies(localPackageVersions[0], version)); if (localPackageVersions.length === 1 && !allVersionsHaveWorkspacePrefix && hasIncompatibilityWithLocalPackageVersion) { // If we saw a version for this dependency that isn't compatible with its actual local package version, add the local package version to the list of versions seen. // Note that using the `workspace:` prefix to refer to the local package version is allowed. versions = [...versions, ...localPackageVersions]; } // Calculate unique versions seen for this dependency. const uniqueVersions = [...new Set(versions)].sort(compareVersionRangesSafe); const uniqueVersionsWithInfo = versionsObjectsWithSortedPackages(uniqueVersions, versionObjectsForDep); return { dependency, versions: uniqueVersionsWithInfo, }; }); } function versionsObjectsWithSortedPackages(versions, versionObjects) { return versions.map((version) => { const matchingVersionObjects = versionObjects.filter((versionObject) => versionObject.version === version); return { version, packages: matchingVersionObjects .map((object) => object.package) .sort((a, b) => Package.comparator(a, b)), }; }); } const HARDCODED_IGNORED_DEPENDENCIES = new Set([ '//', // May be used to add comments to package.json files. ]); export function filterOutIgnoredDependencies(mismatchingVersions, ignoredDependencies, ignoredDependencyPatterns) { for (const ignoreDependency of ignoredDependencies) { if (!mismatchingVersions.some((mismatchingVersion) => mismatchingVersion.dependency === ignoreDependency)) { throw new Error(`Specified option '--ignore-dep ${ignoreDependency}', but no version mismatches detected for this dependency.`); } } for (const ignoredDependencyPattern of ignoredDependencyPatterns) { if (!mismatchingVersions.some((mismatchingVersion) => ignoredDependencyPattern.test(mismatchingVersion.dependency))) { throw new Error(`Specified option '--ignore-dep-pattern ${String(ignoredDependencyPattern)}', but no matching dependencies with version mismatches detected.`); } } if (ignoredDependencies.length > 0 || ignoredDependencyPatterns.length > 0 || mismatchingVersions.some((mismatchingVersion) => HARDCODED_IGNORED_DEPENDENCIES.has(mismatchingVersion.dependency))) { return mismatchingVersions.filter((mismatchingVersion) => !ignoredDependencies.includes(mismatchingVersion.dependency) && !ignoredDependencyPatterns.some((ignoreDependencyPattern) => mismatchingVersion.dependency.match(ignoreDependencyPattern)) && !HARDCODED_IGNORED_DEPENDENCIES.has(mismatchingVersion.dependency)); } return mismatchingVersions; } function writeDependencyVersion(packageJsonPath, packageJsonEndsInNewline, type, dependencyName, newVersion) { const packageJsonEditor = editJsonFile(packageJsonPath, { autosave: true, stringify_eol: packageJsonEndsInNewline, // If a newline at end of file exists, keep it. }); packageJsonEditor.set(`${type}.${dependencyName.replaceAll('.', // Escape dots to avoid creating unwanted nested properties. String.raw `\.`)}`, newVersion, { preservePaths: false }); } // eslint-disable-next-line complexity export function fixVersionsMismatching(packages, mismatchingVersions, dryrun = false) { const fixable = []; const notFixable = []; // Loop through each dependency that has a mismatching versions. for (const mismatchingVersion of mismatchingVersions) { // Decide what version we should fix to. const versions = mismatchingVersion.versions.map((object) => object.version); let fixedVersion; try { fixedVersion = getIncreasedLatestVersion(versions); } catch { // Skip this dependency. notFixable.push(mismatchingVersion); continue; } // If this dependency is from a local package and the version we want to fix to is higher than the actual package version, skip it. const localPackage = packages.find((package_) => package_.name === mismatchingVersion.dependency); if (localPackage && localPackage.packageJson.version && compareVersionRanges(fixedVersion, localPackage.packageJson.version) > 0) { // Skip this dependency. notFixable.push(mismatchingVersion); continue; } if (localPackage && localPackage.packageJson.version === fixedVersion) { // When fixing to the version of a local package, don't just use the bare package version, but include the highest range type we have seen. const highestRangeTypeSeen = getHighestRangeType(versions.map((versionRange) => versionRangeToRange(versionRange))); fixedVersion = `${highestRangeTypeSeen}${String(semver.coerce(fixedVersion))}`; } // Update the dependency version in each package.json. let isFixed = false; for (const package_ of packages) { if (package_.packageJson.devDependencies && package_.packageJson.devDependencies[mismatchingVersion.dependency] && package_.packageJson.devDependencies[mismatchingVersion.dependency] !== fixedVersion) { if (!dryrun) { writeDependencyVersion(package_.pathPackageJson, package_.packageJsonEndsInNewline, DEPENDENCY_TYPE.devDependencies, mismatchingVersion.dependency, fixedVersion); } isFixed = true; } if (package_.packageJson.dependencies && package_.packageJson.dependencies[mismatchingVersion.dependency] && package_.packageJson.dependencies[mismatchingVersion.dependency] !== fixedVersion) { if (!dryrun) { writeDependencyVersion(package_.pathPackageJson, package_.packageJsonEndsInNewline, DEPENDENCY_TYPE.dependencies, mismatchingVersion.dependency, fixedVersion); } isFixed = true; } if (package_.packageJson.optionalDependencies && package_.packageJson.optionalDependencies[mismatchingVersion.dependency] && package_.packageJson.optionalDependencies[mismatchingVersion.dependency] !== fixedVersion) { if (!dryrun) { writeDependencyVersion(package_.pathPackageJson, package_.packageJsonEndsInNewline, DEPENDENCY_TYPE.optionalDependencies, mismatchingVersion.dependency, fixedVersion); } isFixed = true; } if (package_.packageJson.peerDependencies && package_.packageJson.peerDependencies[mismatchingVersion.dependency] && package_.packageJson.peerDependencies[mismatchingVersion.dependency] !== fixedVersion) { if (!dryrun) { writeDependencyVersion(package_.pathPackageJson, package_.packageJsonEndsInNewline, DEPENDENCY_TYPE.peerDependencies, mismatchingVersion.dependency, fixedVersion); } isFixed = true; } if (package_.packageJson.resolutions && package_.packageJson.resolutions[mismatchingVersion.dependency] && package_.packageJson.resolutions[mismatchingVersion.dependency] !== fixedVersion) { if (!dryrun) { writeDependencyVersion(package_.pathPackageJson, package_.packageJsonEndsInNewline, DEPENDENCY_TYPE.resolutions, mismatchingVersion.dependency, fixedVersion); } isFixed = true; } } if (isFixed) { fixable.push(mismatchingVersion); } } return { fixable, notFixable, }; }