From 703a6d82b3d7001aed7456a9216f4dd39397ff97 Mon Sep 17 00:00:00 2001 From: Daniel Rocha Date: Fri, 22 Sep 2023 21:57:30 +0200 Subject: [PATCH] Add `--prettier` flag for Prettier-formatted changelogs (#155) * fix: don't use prettier * feat: rename flag `usePrettier` -> `prettier` * chore: downgrade `prettier` to match `eslint` dependency * refactor: make formatter a sync function * test: add formatter test * chore: update Prettier --- package.json | 2 +- src/changelog.ts | 17 +++++++++-- src/cli.ts | 29 ++++++++++++++++++ src/parse-changelog.ts | 7 +++-- src/update-changelog.ts | 7 ++++- src/validate-changelog.test.ts | 54 ++++++++++++++++++++++++++++++++++ src/validate-changelog.ts | 11 ++++++- 7 files changed, 120 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 2e3026b..873bb7c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dependencies": { "diff": "^5.0.0", "execa": "^5.1.1", + "prettier": "^2.8.8", "semver": "^7.3.5", "yargs": "^17.0.1" }, @@ -58,7 +59,6 @@ "eslint-plugin-prettier": "^4.2.1", "jest": "^26.4.2", "outdent": "^0.8.0", - "prettier": "^2.8.8", "rimraf": "^3.0.2", "ts-jest": "^26.5.6", "typescript": "~4.8.4" diff --git a/src/changelog.ts b/src/changelog.ts index 06842fe..f04addc 100644 --- a/src/changelog.ts +++ b/src/changelog.ts @@ -13,6 +13,11 @@ const changelogDescription = `All notable changes to this project will be docume The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).`; +/** + * Formatter function that formats a Markdown changelog string. + */ +export type Formatter = (changelog: string) => string; + type ReleaseMetadata = { /** * The version of the current release. @@ -257,24 +262,30 @@ export default class Changelog { readonly #tagPrefix: string; + #formatter: Formatter; + /** * Construct an empty changelog. * * @param options - Changelog options. * @param options.repoUrl - The GitHub repository URL for the current project. * @param options.tagPrefix - The prefix used in tags before the version number. + * @param options.formatter - A function that formats the changelog string. */ constructor({ repoUrl, tagPrefix = 'v', + formatter = (changelog) => changelog, }: { repoUrl: string; tagPrefix?: string; + formatter?: Formatter; }) { this.#releases = []; this.#changes = { [unreleased]: {} }; this.#repoUrl = repoUrl; this.#tagPrefix = tagPrefix; + this.#formatter = formatter; } /** @@ -446,8 +457,8 @@ export default class Changelog { * * @returns The stringified changelog. */ - toString() { - return `${changelogTitle} + toString(): string { + const changelog = `${changelogTitle} ${changelogDescription} ${stringifyReleases(this.#releases, this.#changes)} @@ -457,5 +468,7 @@ ${stringifyLinkReferenceDefinitions( this.#tagPrefix, this.#releases, )}`; + + return this.#formatter(changelog); } } diff --git a/src/cli.ts b/src/cli.ts index ea418f4..440c69f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,11 +2,13 @@ import { promises as fs, constants as fsConstants } from 'fs'; import path from 'path'; +import prettier from 'prettier'; import semver from 'semver'; import type { Argv } from 'yargs'; import { hideBin } from 'yargs/helpers'; import yargs from 'yargs/yargs'; +import { Formatter } from './changelog'; import { unreleased, Version } from './constants'; import { generateDiff } from './generate-diff'; import { createEmptyChangelog } from './init'; @@ -88,6 +90,7 @@ type UpdateOptions = { isReleaseCandidate: boolean; projectRootDirectory?: string; tagPrefix: string; + formatter: Formatter; }; /** @@ -100,6 +103,7 @@ type UpdateOptions = { * @param options.repoUrl - The GitHub repository URL for the current project. * @param options.projectRootDirectory - The root project directory. * @param options.tagPrefix - The prefix used in tags before the version number. + * @param options.formatter - A custom Markdown formatter to use. */ async function update({ changelogPath, @@ -108,6 +112,7 @@ async function update({ repoUrl, projectRootDirectory, tagPrefix, + formatter, }: UpdateOptions) { const changelogContent = await readChangelog(changelogPath); @@ -118,6 +123,7 @@ async function update({ isReleaseCandidate, projectRootDirectory, tagPrefixes: [tagPrefix], + formatter, }); if (newChangelogContent) { @@ -135,6 +141,7 @@ type ValidateOptions = { repoUrl: string; tagPrefix: string; fix: boolean; + formatter: Formatter; }; /** @@ -147,6 +154,7 @@ type ValidateOptions = { * @param options.repoUrl - The GitHub repository URL for the current project. * @param options.tagPrefix - The prefix used in tags before the version number. * @param options.fix - Whether to attempt to fix the changelog or not. + * @param options.formatter - A custom Markdown formatter to use. */ async function validate({ changelogPath, @@ -155,6 +163,7 @@ async function validate({ repoUrl, tagPrefix, fix, + formatter, }: ValidateOptions) { const changelogContent = await readChangelog(changelogPath); @@ -165,6 +174,7 @@ async function validate({ repoUrl, isReleaseCandidate, tagPrefix, + formatter, }); return undefined; } catch (error) { @@ -270,6 +280,11 @@ async function main() { 'The current version of the project that the changelog belongs to.', type: 'string', }) + .option('prettier', { + default: false, + description: `Expect the changelog to be formatted with Prettier.`, + type: 'boolean', + }) .epilog(updateEpilog), ) .command( @@ -292,6 +307,11 @@ async function main() { description: `Attempt to fix any formatting errors in the changelog`, type: 'boolean', }) + .option('prettier', { + default: false, + description: `Expect the changelog to be formatted with Prettier.`, + type: 'boolean', + }) .epilog(validateEpilog), ) .command('init', 'Initialize a new empty changelog', (_yargs) => { @@ -311,6 +331,7 @@ async function main() { root: projectRootDirectory, tagPrefix, fix, + prettier: usePrettier, } = argv; let { currentVersion } = argv; @@ -409,6 +430,12 @@ async function main() { } } + const formatter = (changelog: string) => { + return usePrettier + ? prettier.format(changelog, { parser: 'markdown' }) + : changelog; + }; + if (command === 'update') { await update({ changelogPath, @@ -417,6 +444,7 @@ async function main() { repoUrl, projectRootDirectory, tagPrefix, + formatter, }); } else if (command === 'validate') { await validate({ @@ -426,6 +454,7 @@ async function main() { repoUrl, tagPrefix, fix, + formatter, }); } else if (command === 'init') { await init({ diff --git a/src/parse-changelog.ts b/src/parse-changelog.ts index 3a3af07..63dd689 100644 --- a/src/parse-changelog.ts +++ b/src/parse-changelog.ts @@ -1,6 +1,6 @@ import semver from 'semver'; -import Changelog from './changelog'; +import Changelog, { Formatter } from './changelog'; import { ChangeCategory, unreleased } from './constants'; /** @@ -31,19 +31,22 @@ function isValidChangeCategory(category: string): category is ChangeCategory { * @param options.changelogContent - The changelog to parse. * @param options.repoUrl - The GitHub repository URL for the current project. * @param options.tagPrefix - The prefix used in tags before the version number. + * @param options.formatter - A custom Markdown formatter to use. * @returns A changelog instance that reflects the changelog text provided. */ export function parseChangelog({ changelogContent, repoUrl, tagPrefix = 'v', + formatter = undefined, }: { changelogContent: string; repoUrl: string; tagPrefix?: string; + formatter?: Formatter; }) { const changelogLines = changelogContent.split('\n'); - const changelog = new Changelog({ repoUrl, tagPrefix }); + const changelog = new Changelog({ repoUrl, tagPrefix, formatter }); const unreleasedHeaderIndex = changelogLines.indexOf(`## [${unreleased}]`); if (unreleasedHeaderIndex === -1) { diff --git a/src/update-changelog.ts b/src/update-changelog.ts index 9ee9c4b..9e73c5d 100644 --- a/src/update-changelog.ts +++ b/src/update-changelog.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'assert'; import execa from 'execa'; import type Changelog from './changelog'; +import { Formatter } from './changelog'; import { ChangeCategory, Version } from './constants'; import { parseChangelog } from './parse-changelog'; @@ -162,6 +163,7 @@ export type UpdateChangelogOptions = { isReleaseCandidate: boolean; projectRootDirectory?: string; tagPrefixes?: [string, ...string[]]; + formatter?: Formatter; }; /** @@ -183,6 +185,7 @@ export type UpdateChangelogOptions = { * current git repository. * @param options.tagPrefixes - A list of tag prefixes to look for, where the first is the intended * prefix and each subsequent prefix is a fallback in case the previous tag prefixes are not found. + * @param options.formatter - A custom Markdown formatter to use. * @returns The updated changelog text. */ export async function updateChangelog({ @@ -192,7 +195,8 @@ export async function updateChangelog({ isReleaseCandidate, projectRootDirectory, tagPrefixes = ['v'], -}: UpdateChangelogOptions) { + formatter = undefined, +}: UpdateChangelogOptions): Promise { if (isReleaseCandidate && !currentVersion) { throw new Error( `A version must be specified if 'isReleaseCandidate' is set.`, @@ -202,6 +206,7 @@ export async function updateChangelog({ changelogContent, repoUrl, tagPrefix: tagPrefixes[0], + formatter, }); // Ensure we have all tags on remote diff --git a/src/validate-changelog.test.ts b/src/validate-changelog.test.ts index 0d7ef6d..396a753 100644 --- a/src/validate-changelog.test.ts +++ b/src/validate-changelog.test.ts @@ -1,3 +1,5 @@ +import prettier from 'prettier'; + import { validateChangelog } from './validate-changelog'; const emptyChangelog = `# Changelog @@ -94,6 +96,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 `; +const prettierChangelog = `# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.1.1] - 2023-03-05 + +### Added + +- Feature A +- Feature B + +### Changed + +- Feature D + +### Fixed + +- Bug C + +## [1.0.0] - 2017-06-20 + +### Added + +- Feature A +- Feature B + +[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.1.1...HEAD +[1.1.1]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...v1.1.1 +[1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 +`; + describe('validateChangelog', () => { it('should not throw for any empty valid changelog', () => { expect(() => @@ -632,4 +670,20 @@ describe('validateChangelog', () => { ).not.toThrow(); }); }); + + describe('formatted changelog', () => { + it("doesn't throw if the changelog is formatted with prettier", () => { + expect(() => + validateChangelog({ + changelogContent: prettierChangelog, + currentVersion: '1.1.1', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + formatter: (changelog) => + prettier.format(changelog, { parser: 'markdown' }), + }), + ).not.toThrow(); + }); + }); }); diff --git a/src/validate-changelog.ts b/src/validate-changelog.ts index 2d3e727..046fc91 100644 --- a/src/validate-changelog.ts +++ b/src/validate-changelog.ts @@ -1,3 +1,4 @@ +import { Formatter } from './changelog'; import { Version, ChangeCategory } from './constants'; import { parseChangelog } from './parse-changelog'; @@ -71,6 +72,7 @@ type ValidateChangelogOptions = { repoUrl: string; isReleaseCandidate: boolean; tagPrefix?: string; + formatter?: Formatter; }; /** @@ -87,6 +89,7 @@ type ValidateChangelogOptions = { * also ensure the current version is represented in the changelog with a * header, and that there are no unreleased changes present. * @param options.tagPrefix - The prefix used in tags before the version number. + * @param options.formatter - A custom Markdown formatter to use. * @throws `InvalidChangelogError` - Will throw if the changelog is invalid * @throws `MissingCurrentVersionError` - Will throw if `isReleaseCandidate` is * `true` and the changelog is missing the release header for the current @@ -103,8 +106,14 @@ export function validateChangelog({ repoUrl, isReleaseCandidate, tagPrefix = 'v', + formatter = undefined, }: ValidateChangelogOptions) { - const changelog = parseChangelog({ changelogContent, repoUrl, tagPrefix }); + const changelog = parseChangelog({ + changelogContent, + repoUrl, + tagPrefix, + formatter, + }); const hasUnreleasedChanges = Object.keys(changelog.getUnreleasedChanges()).length !== 0; const releaseChanges = currentVersion