import { consola } from 'consola'; import { readJsonSync, writeJSONSync } from 'fs-extra'; import { markdownToTxt } from 'markdown-to-txt'; import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import semver from 'semver'; import { CHANGELOG_DIR, CHANGELOG_FILE } from './const'; export interface ChangelogStaticItem { children: { [category: string]: string[]; }; date: string; version: string; } class BuildStaticChangelog { private removeDetailsTag = (changelog: string): string => { const detailsRegex: RegExp = /]*>[\S\s]*?<\/details>/gi; return changelog.replaceAll(detailsRegex, ''); }; private cleanVersion = (version: string): string => { return semver.clean(version) || version; }; private formatCategory = (category: string): string => { const cate = category.trim().toLowerCase(); switch (cate) { case 'bug fixes': { return 'fixes'; } case 'features': { return 'features'; } default: { return 'improvements'; } } }; private formatChangelog = (changelog: string): ChangelogStaticItem[] => { const cleanedChangelog = this.removeDetailsTag(changelog); const input = markdownToTxt(cleanedChangelog); const versions = input.split(/Version |Version /).slice(1); const output: ChangelogStaticItem[] = []; for (const version of versions) { const lines = version.trim().split('\n'); const versionNumber = lines[0].trim(); const date = lines[2].replace('Released on ', '').trim(); const entry: ChangelogStaticItem = { children: {}, date: date, version: this.cleanVersion(versionNumber), }; let currentCategory = ''; let skipSection = false; for (let i = 3; i < lines.length; i++) { const line = lines[i].trim(); if (line === '') continue; if (/^\p{Emoji}/u.test(line)) { currentCategory = this.formatCategory(line.replace(/^\p{Emoji} /u, '')); if (!currentCategory) continue; entry.children[currentCategory] = []; skipSection = false; } else if (line.startsWith('misc:') && !skipSection && currentCategory) { entry.children[currentCategory].push(line.replace('misc:', '').trim()); } } // Remove empty categories for (const category in entry.children) { if (entry.children[category].length === 0) { delete entry.children[category]; } } output.push(entry); } return output; }; private mergeAndSortVersions = (oldVersions: any, newVersions: any) => { const mergedVersions = [...oldVersions]; for (const newVersion of newVersions) { const existingIndex = mergedVersions.findIndex( (v) => this.cleanVersion(v.version) === this.cleanVersion(newVersion.version), ); if (existingIndex === -1) { const insertIndex = mergedVersions.findIndex( (v) => semver.compare(this.cleanVersion(newVersion.version), this.cleanVersion(v.version)) > 0, ); if (insertIndex === -1) { mergedVersions.push(newVersion); } else { mergedVersions.splice(insertIndex, 0, newVersion); } } } return mergedVersions; }; run() { Object.entries(CHANGELOG_FILE).forEach(([version, path]) => { const data = readFileSync(path, 'utf8'); const newFile = this.formatChangelog(data); const filename = resolve(CHANGELOG_DIR, `${version}.json`); let mergedFile = newFile; if (existsSync(filename)) { const oldFile = readJsonSync(filename, 'utf8'); mergedFile = this.mergeAndSortVersions(oldFile, newFile); } writeJSONSync(filename, mergedFile, { spaces: 2 }); consola.success(`Changelog ${version} has been built successfully!`); }); } } export const buildStaticChangelog = new BuildStaticChangelog();