diff --git a/.changeset/five-crews-allow.md b/.changeset/five-crews-allow.md new file mode 100644 index 0000000..0a3b120 --- /dev/null +++ b/.changeset/five-crews-allow.md @@ -0,0 +1,5 @@ +--- +"@manypkg/cli": minor +--- + +Add an option to allow multiple versions for external mismatch check diff --git a/packages/cli/src/checks/EXTERNAL_MISMATCH.ts b/packages/cli/src/checks/EXTERNAL_MISMATCH.ts index 8b00a38..aea6ec0 100644 --- a/packages/cli/src/checks/EXTERNAL_MISMATCH.ts +++ b/packages/cli/src/checks/EXTERNAL_MISMATCH.ts @@ -1,6 +1,7 @@ import { makeCheck, getMostCommonRangeMap, + getClosestAllowedRange, NORMAL_DEPENDENCY_TYPES, } from "./utils"; import { Package } from "@manypkg/get-packages"; @@ -15,7 +16,7 @@ type ErrorType = { }; export default makeCheck({ - validate: (workspace, allWorkspace) => { + validate: (workspace, allWorkspace, rootWorkspace, options) => { let errors: ErrorType[] = []; let mostCommonRangeMap = getMostCommonRangeMap(allWorkspace); for (let depType of NORMAL_DEPENDENCY_TYPES) { @@ -25,9 +26,13 @@ export default makeCheck({ for (let depName in deps) { let range = deps[depName]; let mostCommonRange = mostCommonRangeMap.get(depName); + const allowedVersions = + options.allowedDependencyVersions && + options.allowedDependencyVersions[depName]; if ( mostCommonRange !== undefined && mostCommonRange !== range && + !(allowedVersions && allowedVersions.includes(range)) && validRange(range) ) { errors.push({ @@ -35,7 +40,9 @@ export default makeCheck({ workspace, dependencyName: depName, dependencyRange: range, - mostCommonDependencyRange: mostCommonRange, + mostCommonDependencyRange: allowedVersions + ? getClosestAllowedRange(range, allowedVersions) + : mostCommonRange, }); } } diff --git a/packages/cli/src/checks/__tests__/EXTERNAL_MISMATCH.ts b/packages/cli/src/checks/__tests__/EXTERNAL_MISMATCH.ts index ee7f17a..32cc591 100644 --- a/packages/cli/src/checks/__tests__/EXTERNAL_MISMATCH.ts +++ b/packages/cli/src/checks/__tests__/EXTERNAL_MISMATCH.ts @@ -42,7 +42,7 @@ it("should error if the ranges are valid and they are not equal", () => { `); }); -it("should error and return the correct mostCommonDependencyRange when the ranges are valid, they are not equal and there are more than 2", () => { +it("should error and return the correct expectedRange when the ranges are valid, they are not equal and there are more than 2", () => { let ws = getWS(); ws.get("pkg-1")!.packageJson.dependencies = { something: "1.0.0" }; @@ -92,7 +92,7 @@ it("should error and return the correct mostCommonDependencyRange when the range `); }); -it("should error and return the correct mostCommonDependencyRange when the ranges are valid, but the 2nd dependnecy is most common", () => { +it("should error and return the correct expectedRange when the ranges are valid, but the 2nd dependnecy is most common", () => { let ws = getWS(); ws.get("pkg-1")!.packageJson.dependencies = { something: "2.0.0" }; @@ -143,7 +143,7 @@ it("should error and return the correct mostCommonDependencyRange when the range expect(errors.length).toEqual(0); }); -it("should error and return the correct mostCommonDependencyRange when the ranges are valid, but everything wants a different version", () => { +it("should error and return the correct expectedRange when the ranges are valid, but everything wants a different version", () => { let ws = getWS(); ws.get("pkg-1")!.packageJson.dependencies = { something: "1.0.0" }; @@ -234,3 +234,141 @@ it("should not error if the value is not a valid semver range", () => { errors = internalMismatch.validate(ws.get("pkg-1")!, ws, rootWorkspace, {}); expect(errors.length).toEqual(0); }); + +it("should not error if the range is included in the allowedDependencyVersions option", () => { + let ws = getWS(); + + ws.get("pkg-1")!.packageJson.dependencies = { something: "1.0.0" }; + + let pkg2 = getFakeWS("pkg-2"); + pkg2.packageJson.dependencies = { + something: "2.0.0", + }; + ws.set("pkg-2", pkg2); + + const options = { + allowedDependencyVersions: { + something: ["1.0.0", "2.0.0"], + }, + }; + + let errors = internalMismatch.validate(pkg2, ws, rootWorkspace, options); + expect(errors.length).toEqual(0); + + errors = internalMismatch.validate( + ws.get("pkg-1")!, + ws, + rootWorkspace, + options + ); + expect(errors.length).toEqual(0); +}); + +it("should error and fix the version to the closest allowed one when adding an allowed major", () => { + const options = { + allowedDependencyVersions: { + something: ["^1.0.0", "2.0.0"], + }, + }; + + let ws = getWS(); + + ws.get("pkg-1")!.packageJson.dependencies = { something: "^1.0.0" }; + + // version 1.0.0 is the most commonly used one + let pkg1a = getFakeWS("pkg-1a"); + pkg1a.packageJson.dependencies = { + something: "^1.0.0", + }; + ws.set("pkg-1a", pkg1a); + + // version 2.0.0 is allowed + let pkg2 = getFakeWS("pkg-2"); + pkg2.packageJson.dependencies = { + something: "2.0.0", + }; + ws.set("pkg-2", pkg2); + + // try to add version 2.1.0 + let pkg2a = getFakeWS("pkg-2a"); + pkg2a.packageJson.dependencies = { + something: "^2.1.0", + }; + ws.set("pkg-2a", pkg2a); + + let errors = internalMismatch.validate(pkg2a, ws, rootWorkspace, options); + expect(errors.length).toEqual(1); + expect(errors[0]).toEqual( + expect.objectContaining({ + dependencyName: "something", + dependencyRange: "^2.1.0", + mostCommonDependencyRange: "2.0.0", + }) + ); + internalMismatch.fix(errors[0], options); + expect(pkg2a.packageJson.dependencies.something).toEqual("2.0.0"); + + // try to add version 1.0.1 + let pkg1b = getFakeWS("pkg-1b"); + pkg1b.packageJson.dependencies = { + something: "^1.0.1", + }; + ws.set("pkg-1b", pkg1b); + + errors = internalMismatch.validate(pkg1b, ws, rootWorkspace, options); + expect(errors.length).toEqual(1); + expect(errors[0]).toEqual( + expect.objectContaining({ + dependencyName: "something", + dependencyRange: "^1.0.1", + mostCommonDependencyRange: "^1.0.0", + }) + ); + internalMismatch.fix(errors[0], options); + expect(pkg1b.packageJson.dependencies.something).toEqual("^1.0.0"); +}); + +it("should error and fix the version to the highest allowed one when adding a newer major", () => { + const options = { + allowedDependencyVersions: { + something: ["1.0.0", "^2.0.0"], + }, + }; + + let ws = getWS(); + + ws.get("pkg-1")!.packageJson.dependencies = { something: "1.0.0" }; + + // version 1.0.0 is the most commonly used one + let pkg1a = getFakeWS("pkg-1a"); + pkg1a.packageJson.dependencies = { + something: "1.0.0", + }; + ws.set("pkg-1a", pkg1a); + + // version 2.0.0 is allowed + let pkg2 = getFakeWS("pkg-2"); + pkg2.packageJson.dependencies = { + something: "^2.0.0", + }; + ws.set("pkg-2", pkg2); + + // try to add version 3.0.0 + let pkg3 = getFakeWS("pkg-3"); + pkg3.packageJson.dependencies = { + something: "3.0.0", + }; + ws.set("pkg-3", pkg3); + + let errors = internalMismatch.validate(pkg3, ws, rootWorkspace, options); + expect(errors.length).toEqual(1); + expect(errors[0]).toEqual( + expect.objectContaining({ + dependencyName: "something", + dependencyRange: "3.0.0", + mostCommonDependencyRange: "^2.0.0", + }) + ); + internalMismatch.fix(errors[0], options); + expect(pkg3.packageJson.dependencies.something).toEqual("^2.0.0"); +}); diff --git a/packages/cli/src/checks/utils.ts b/packages/cli/src/checks/utils.ts index 473bc14..617a0cd 100644 --- a/packages/cli/src/checks/utils.ts +++ b/packages/cli/src/checks/utils.ts @@ -1,6 +1,8 @@ import { Package } from "@manypkg/get-packages"; import * as semver from "semver"; import { highest } from "sembear"; +import * as logger from "../logger"; +import { ExitError } from "../errors"; export const NORMAL_DEPENDENCY_TYPES = [ "dependencies", @@ -19,6 +21,7 @@ export type Options = { defaultBranch?: string; ignoredRules?: string[]; workspaceProtocol?: "allow" | "require"; + allowedDependencyVersions?: { [dependency: string]: string[] }; }; type RootCheck = { @@ -185,6 +188,32 @@ export function isArrayEqual(arrA: Array, arrB: Array) { return true; } +export function getClosestAllowedRange( + range: string, + allowedVersions: string[] +) { + const major = semver.major(getVersionFromRange(range)); + const allowedVersionsWithSameMajor = allowedVersions.filter( + (version) => semver.major(getVersionFromRange(version)) === major + ); + const possibleRanges = + allowedVersionsWithSameMajor.length > 0 + ? allowedVersionsWithSameMajor + : allowedVersions; + return possibleRanges.sort((a, b) => + semver.gt(getVersionFromRange(a), getVersionFromRange(b)) ? -1 : 1 + )[0]; +} + +function getVersionFromRange(range: string) { + const minVersion = semver.minVersion(range); + if (minVersion) { + return minVersion; + } + logger.error(`Invalid range: ${range}`); + throw new ExitError(1); +} + function makeCheck( check: RootCheckWithFix ): RootCheckWithFix;