diff --git a/src/bump/bump-check.test.ts b/src/bump/bump-check.test.ts new file mode 100644 index 0000000..8bdb796 --- /dev/null +++ b/src/bump/bump-check.test.ts @@ -0,0 +1,61 @@ +import chalk from "chalk"; +import latestVersion from "latest-version"; + +import { bumpCheck } from "./bump-check"; + +import { BumpObject } from "./interfaces/bump-object"; + +describe("Bump check", () => { + let results: any; + + beforeEach(() => { + results = []; + jest + .spyOn(process.stdout, "write") + .mockImplementation(val => results.push(String(val).trim())); + }); + + afterEach(() => jest.restoreAllMocks()); + + test("No updates needed", async () => { + jest.spyOn(latestVersion, "default").mockResolvedValue("1.0.0"); + + const validList: BumpObject[] = [ + { + packageFile: { name: "test-1", version: "1.0.0" }, + packagePath: "src/test", + bumpedVersion: "1.0.1", + }, + ]; + + const checkedList = await bumpCheck(validList); + expect(checkedList).toEqual(validList); + }); + + test("Updates needed", async () => { + jest.spyOn(latestVersion, "default").mockResolvedValue("1.0.1"); + + const bumpList: BumpObject[] = [ + { + packageFile: { name: "test-1", version: "1.0.0" }, + packagePath: "src/test", + bumpedVersion: "1.0.1", + }, + ]; + const validList: BumpObject[] = [ + { + packageFile: { name: "test-1", version: "1.0.0" }, + packagePath: "src/test", + bumpedVersion: "1.0.2", + }, + ]; + + const checkedList = await bumpCheck(bumpList); + expect(checkedList).toEqual(validList); + expect(results).toEqual( + chalk.red( + "test-1 version mismatch. NPM version 1.0.1. Bump value 1.0.1. Auto-bumping..." + ) + ); + }); +}); diff --git a/src/bump/bump-check.ts b/src/bump/bump-check.ts new file mode 100644 index 0000000..2d04651 --- /dev/null +++ b/src/bump/bump-check.ts @@ -0,0 +1,56 @@ +import chalk from "chalk"; +import latestVersion from "latest-version"; +import PQueue from "p-queue"; +import semver from "semver"; + +import { bumpValue } from "./bump-value"; +import log from "../utils/log"; + +import { BumpObject } from "./interfaces/bump-object"; + +const queue = new PQueue({ concurrency: 6 }); + +const validate = async (item: BumpObject) => { + let npmVersion: string | boolean; + + try { + // Get latest version from NPM registry and compare if bumped version is greater than NPM + npmVersion = await latestVersion(item.packageFile.name); + if (semver.gt(item.bumpedVersion, npmVersion)) { + return item; + } + } catch { + // Assume package isn't published on NPM yet + return item; + } + + // If bumped value is not greater than NPM, auto bump with patch + const newItem = item; + const newVersion = bumpValue(npmVersion, "patch"); + if (newVersion) { + newItem.bumpedVersion = newVersion; + return newItem; + } + + // If failed to bump again, will not publish + newItem.failedValidation = true; + log( + chalk.red( + `${newItem.packageFile.name} version mismatch. Failed to bump. Not publishing.` + ) + ); + return newItem; +}; + +const bumpCheck = async (bumpList: BumpObject[]): Promise => { + const checkedList: Promise[] = []; + + for (const item of bumpList) { + const validatedItem = queue.add(() => validate(item)); + checkedList.push(validatedItem); + } + + return Promise.all(checkedList); +}; + +export { bumpCheck }; diff --git a/src/bump/bump-value.test.ts b/src/bump/bump-value.test.ts new file mode 100644 index 0000000..17aa4df --- /dev/null +++ b/src/bump/bump-value.test.ts @@ -0,0 +1,28 @@ +import { bumpValue } from "./bump-value"; + +describe("Bump Value", () => { + test("Patch", () => { + const newVersion = bumpValue("1.0.0", "patch"); + expect(newVersion).toBe("1.0.1"); + }); + + test("Minor", () => { + const newVersion = bumpValue("1.0.0", "minor"); + expect(newVersion).toBe("1.1.0"); + }); + + test("Major", () => { + const newVersion = bumpValue("1.0.0", "major"); + expect(newVersion).toBe("2.0.0"); + }); + + test("Version number", () => { + const newVersion = bumpValue("1.0.0", "2.5.1"); + expect(newVersion).toBe("2.5.1"); + }); + + test("False", () => { + const newVersion = bumpValue("a.b.c", "major"); + expect(newVersion).toBeFalsy(); + }); +}); diff --git a/src/bump/bump-value.ts b/src/bump/bump-value.ts new file mode 100644 index 0000000..11c5638 --- /dev/null +++ b/src/bump/bump-value.ts @@ -0,0 +1,32 @@ +import semver from "semver"; + +const bumpValue = (oldVersion: string, bumpArg: string): string | false => { + // Check if valid semver version and if invalid return false + const arr = semver.valid(oldVersion)?.split("."); + if (arr) { + if (bumpArg === "patch") { + const newNum = Number(arr[2]) + 1; + arr[2] = String(newNum); + return arr.join("."); + } + if (bumpArg === "minor") { + const newNum = Number(arr[1]) + 1; + arr[1] = String(newNum); + return arr.join("."); + } + if (bumpArg === "major") { + const newNum = Number(arr[0]) + 1; + arr[0] = String(newNum); + return arr.join("."); + } + // Else just return number version + if (semver.valid(bumpArg)) { + return bumpArg; + } + + return false; + } + return false; +}; + +export { bumpValue }; diff --git a/src/bump/bump-write.test.ts b/src/bump/bump-write.test.ts new file mode 100644 index 0000000..d737624 --- /dev/null +++ b/src/bump/bump-write.test.ts @@ -0,0 +1,25 @@ +import jsonfile from "jsonfile"; + +import { bumpWrite } from "./bump-write"; + +import { BumpObject } from "./interfaces/bump-object"; + +describe("Bump write function", () => { + test("Should not throw", async () => { + jest.spyOn(jsonfile, "writeFile").mockResolvedValue(); + + const bumpList: BumpObject[] = [ + { + packageFile: { name: "test-package", version: "1.0.0" }, + packagePath: "src/publish/fixtures/", + bumpedVersion: "1.1.0", + }, + { + packageFile: { name: "test-package2", version: "1.1.0" }, + packagePath: "src/publish/fixtures/", + bumpedVersion: "1.2.0", + }, + ]; + await expect(bumpWrite(bumpList)).resolves.not.toThrow(); + }); +}); diff --git a/src/bump/bump-write.ts b/src/bump/bump-write.ts new file mode 100644 index 0000000..27734be --- /dev/null +++ b/src/bump/bump-write.ts @@ -0,0 +1,24 @@ +import jsonfile from "jsonfile"; +import path from "path"; + +import { BumpObject } from "./interfaces/bump-object"; + +const writeUpdate = async (item: BumpObject) => { + const newPackageFile = item.packageFile; + newPackageFile.version = item.bumpedVersion; + + await jsonfile.writeFile( + path.join(item.packagePath, "package.json"), + newPackageFile + ); +}; + +const bumpWrite = (bumpList: BumpObject[]): Promise => { + const promises = []; + for (const item of bumpList) { + promises.push(writeUpdate(item)); + } + return Promise.all(promises); +}; + +export { bumpWrite }; diff --git a/src/bump/bump.ts b/src/bump/bump.ts new file mode 100644 index 0000000..85a7327 --- /dev/null +++ b/src/bump/bump.ts @@ -0,0 +1,58 @@ +import async from "async"; +import chalk from "chalk"; +import latestVersion from "latest-version"; +import path from "path"; +import jsonfile from "jsonfile"; +import semver from "semver"; + +import log from "../utils/log"; +import { pathToPackage } from "../utils/path-to-package"; + +import { PackageJson } from "../utils/interfaces/package-json"; +import { QueueObject } from "./interfaces/queue-object"; +import { BumpObject } from "./interfaces/bump-object"; + +const processQueue = async ({ + packagePath, + packageFile, + bumpArg, + verify, +}: QueueObject) => { + let npmVersion: string | false; + // Get latest version from NPM registry + try { + npmVersion = await latestVersion(packageFile.name); + } catch { + // Assume package isn't published + npmVersion = false; + } + + // Bump current package.json + const currentVersion = packageFile.version; + const bumpedVersion = bumpValue(currentVersion, bumpArg); + + if (bumpedVersion && npmVersion) { + // Compare if bumped version is greater than version on NPM + if (semver.gt(bumpedVersion, npmVersion)) { + await writeUpdate(packageFile, packagePath, bumpedVersion); + } else { + // Try bumping one more time this time + const newBumpedVersion = bumpValue(bumpedVersion, "patch"); + if (newBumpedVersion && semver.gt(newBumpedVersion, npmVersion)) { + await writeUpdate(packageFile, packagePath, newBumpedVersion); + } else { + log( + chalk.red( + `${packageFile.name} failed to bump. Specified bump version is less than NPM version.` + ) + ); + } + } + } else { + log( + chalk.red( + `${packageFile.name} failed to bump. Possible failure due to bump value or incompatible existing package.json value.` + ) + ); + } +}; diff --git a/src/bump/interfaces/bump-object.ts b/src/bump/interfaces/bump-object.ts new file mode 100644 index 0000000..e0066f1 --- /dev/null +++ b/src/bump/interfaces/bump-object.ts @@ -0,0 +1,8 @@ +import { PackageJson } from "../../utils/interfaces/package-json"; + +export interface BumpObject { + packageFile: PackageJson; + packagePath: string; + bumpedVersion: string; + failedValidation?: boolean; +} diff --git a/src/bump/interfaces/queue-object.ts b/src/bump/interfaces/queue-object.ts new file mode 100644 index 0000000..fc9a8c1 --- /dev/null +++ b/src/bump/interfaces/queue-object.ts @@ -0,0 +1,8 @@ +import { PackageJson } from "../../utils/interfaces/package-json"; + +export interface QueueObject { + packageFile: PackageJson; + packagePath: string; + bumpArg: string; + verify: boolean; +} diff --git a/src/changed/interfaces/config.ts b/src/changed/interfaces/config.ts index 91f76d1..827a22a 100644 --- a/src/changed/interfaces/config.ts +++ b/src/changed/interfaces/config.ts @@ -4,4 +4,7 @@ export interface Config { commitMessage: string; commitFrom: string; commitTo?: string; + noVerify?: boolean; + autoBump?: boolean; + yes?: boolean; } diff --git a/src/commands/bump.ts b/src/commands/bump.ts index eb33931..edda2ba 100644 --- a/src/commands/bump.ts +++ b/src/commands/bump.ts @@ -1,4 +1,9 @@ import { Command, flags } from "@oclif/command"; +import { cli } from "cli-ux"; + +import chalk from "chalk"; +import { readConfig } from "../changed/read-config"; +import { findDiff } from "../changed/find-diff"; export default class Bump extends Command { static description = "Bumps package versions"; @@ -9,11 +14,49 @@ export default class Bump extends Command { "no-verify": flags.boolean({ description: "Skip checking NPM registry for conflicting versions", }), + // flag to patch auto-bump a package if version matches NPM version --auto-bump + "auto-bump": flags.boolean({ + description: "Auto-bump package if specified version is too low", + }), + // flag to skip confirmation to write package bumps --yes + yes: flags.boolean({ + description: "Skip confirmation to write bumped versions to package.json", + }), }; static args = [{ name: "version" }]; async run(): Promise { const { args, flags } = this.parse(Bump); + + const config = await readConfig(); + const diff = await findDiff(config); + + // Args + const bumpArg = args.version; + + // Flags + let verify = true || config.noVerify; + if (flags["no-verify"]) verify = false; + + let autoBump = false || config.autoBump; + if (flags["auto-bump"]) autoBump = true; + + // If there are no packages to publish, no need for confirmation prompt + let skipPrompt = false || config.yes; + if (flags.yes) skipPrompt = true; + + /* const bumpedFiles = await bumpList(diff, bumpArg, verify); + if (bumpedFiles) { + if (skipPrompt) { + await bumpPackages(bumpedFiles); + } else { + const input = await cli.confirm(`Bump ${bumpedFiles.length} packages?`); + if (input) { + await bumpPackages(bumpedFiles); + cli.log(chalk.green.bold(`Packages bumped!`)); + } + } + } */ } } diff --git a/src/publish/bump.ts b/src/publish/bump.ts deleted file mode 100644 index b39d1ab..0000000 --- a/src/publish/bump.ts +++ /dev/null @@ -1 +0,0 @@ -const bump = async diff => {};