Skip to content

Commit

Permalink
Add --prettier flag for Prettier-formatted changelogs (#155)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
danroc authored Sep 22, 2023
1 parent 9159de5 commit 703a6d8
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 7 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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"
Expand Down
17 changes: 15 additions & 2 deletions src/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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)}
Expand All @@ -457,5 +468,7 @@ ${stringifyLinkReferenceDefinitions(
this.#tagPrefix,
this.#releases,
)}`;

return this.#formatter(changelog);
}
}
29 changes: 29 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -88,6 +90,7 @@ type UpdateOptions = {
isReleaseCandidate: boolean;
projectRootDirectory?: string;
tagPrefix: string;
formatter: Formatter;
};

/**
Expand All @@ -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,
Expand All @@ -108,6 +112,7 @@ async function update({
repoUrl,
projectRootDirectory,
tagPrefix,
formatter,
}: UpdateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -118,6 +123,7 @@ async function update({
isReleaseCandidate,
projectRootDirectory,
tagPrefixes: [tagPrefix],
formatter,
});

if (newChangelogContent) {
Expand All @@ -135,6 +141,7 @@ type ValidateOptions = {
repoUrl: string;
tagPrefix: string;
fix: boolean;
formatter: Formatter;
};

/**
Expand All @@ -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,
Expand All @@ -155,6 +163,7 @@ async function validate({
repoUrl,
tagPrefix,
fix,
formatter,
}: ValidateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -165,6 +174,7 @@ async function validate({
repoUrl,
isReleaseCandidate,
tagPrefix,
formatter,
});
return undefined;
} catch (error) {
Expand Down Expand Up @@ -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(
Expand All @@ -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) => {
Expand All @@ -311,6 +331,7 @@ async function main() {
root: projectRootDirectory,
tagPrefix,
fix,
prettier: usePrettier,
} = argv;
let { currentVersion } = argv;

Expand Down Expand Up @@ -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,
Expand All @@ -417,6 +444,7 @@ async function main() {
repoUrl,
projectRootDirectory,
tagPrefix,
formatter,
});
} else if (command === 'validate') {
await validate({
Expand All @@ -426,6 +454,7 @@ async function main() {
repoUrl,
tagPrefix,
fix,
formatter,
});
} else if (command === 'init') {
await init({
Expand Down
7 changes: 5 additions & 2 deletions src/parse-changelog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import semver from 'semver';

import Changelog from './changelog';
import Changelog, { Formatter } from './changelog';
import { ChangeCategory, unreleased } from './constants';

/**
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion src/update-changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -162,6 +163,7 @@ export type UpdateChangelogOptions = {
isReleaseCandidate: boolean;
projectRootDirectory?: string;
tagPrefixes?: [string, ...string[]];
formatter?: Formatter;
};

/**
Expand All @@ -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({
Expand All @@ -192,7 +195,8 @@ export async function updateChangelog({
isReleaseCandidate,
projectRootDirectory,
tagPrefixes = ['v'],
}: UpdateChangelogOptions) {
formatter = undefined,
}: UpdateChangelogOptions): Promise<string | undefined> {
if (isReleaseCandidate && !currentVersion) {
throw new Error(
`A version must be specified if 'isReleaseCandidate' is set.`,
Expand All @@ -202,6 +206,7 @@ export async function updateChangelog({
changelogContent,
repoUrl,
tagPrefix: tagPrefixes[0],
formatter,
});

// Ensure we have all tags on remote
Expand Down
54 changes: 54 additions & 0 deletions src/validate-changelog.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import prettier from 'prettier';

import { validateChangelog } from './validate-changelog';

const emptyChangelog = `# Changelog
Expand Down Expand Up @@ -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(() =>
Expand Down Expand Up @@ -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();
});
});
});
11 changes: 10 additions & 1 deletion src/validate-changelog.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Formatter } from './changelog';
import { Version, ChangeCategory } from './constants';
import { parseChangelog } from './parse-changelog';

Expand Down Expand Up @@ -71,6 +72,7 @@ type ValidateChangelogOptions = {
repoUrl: string;
isReleaseCandidate: boolean;
tagPrefix?: string;
formatter?: Formatter;
};

/**
Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 703a6d8

Please sign in to comment.