diff --git a/CHANGELOG.yml b/CHANGELOG.yml index 2dd90d9..50de369 100644 --- a/CHANGELOG.yml +++ b/CHANGELOG.yml @@ -1,12 +1,16 @@ project: - name: null - homepage: null + name: changelord + homepage: https://git.babyl.ca/yanick/changelord.js with_stats: true ticket_url: null releases: - version: NEXT - changes: [] + changes: + - port the Perl changelord to JavaScript. change_types: + - title: '' + level: minor + keywords: [''] - title: Features level: minor keywords: diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..0692409 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,6 @@ +# https://taskfile.dev +version: '3' + +tasks: + test: vitest run src + test:dev: vitest src diff --git a/package.json b/package.json index 3763c9e..0fcac03 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,13 @@ "dependencies": { "consola": "^3.1.0", "fs-extra": "^11.1.1", + "markdown-utils": "^1.0.0", + "remeda": "^1.14.0", "yaml": "^2.2.2", "yargs": "^17.7.2" }, "devDependencies": { - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "vitest": "^0.31.0" } } diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 0000000..3789e21 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,9 @@ +export default { + endOfLine: "lf", + semi: true, + singleQuote: false, + tabWidth: 2, + trailingComma: "es5", + bracketSpacing: true, + proseWrap: "always", +}; diff --git a/src/changelord.js b/src/changelord.js index 0e1d1a6..42acf55 100755 --- a/src/changelord.js +++ b/src/changelord.js @@ -3,6 +3,8 @@ import { hideBin } from 'yargs/helpers'; import yargs from 'yargs'; import { join } from 'path'; +import yaml from 'yaml'; +import fs from 'fs-extra'; import print from './command/print.js' import init from './command/init.js' @@ -11,9 +13,10 @@ import consola from 'consola'; consola.raw = (...args) => console.log(...args); + yargs(hideBin(process.argv)) .config({ - consola + consola, }) .default('source', join( process.cwd(), 'CHANGELOG.yml' )) .describe('source', 'changelog source') diff --git a/src/command/init.js b/src/command/init.js index 6db14b6..c417937 100644 --- a/src/command/init.js +++ b/src/command/init.js @@ -1,50 +1,52 @@ -import fs from 'fs-extra'; -import { consola } from 'consola'; -import { stringify } from 'yaml'; +import fs from "fs-extra"; +import { consola } from "consola"; +import { stringify } from "yaml"; const change_types = [ - { title: 'Features' , level: 'minor', keywords: [ 'feat' ] } , - { title : 'Bug fixes' , level : 'patch', keywords : [ 'fix' ] }, - { title : 'Package maintenance' , level : 'patch', keywords : [ 'chore', 'maint', 'refactor' ] }, - { title : 'Statistics' , level : 'patch', keywords : [ 'stats' ] }, - ]; + { title: "", level: "minor", keywords: [""] }, + { title: "Features", level: "minor", keywords: ["feat"] }, + { title: "Bug fixes", level: "patch", keywords: ["fix"] }, + { + title: "Package maintenance", + level: "patch", + keywords: ["chore", "maint", "refactor"], + }, + { title: "Statistics", level: "patch", keywords: ["stats"] }, +]; - -const base_changelog = { - project: { - name: null, - homepage: null, - with_stats: true, - ticket_url: null, - }, - releases: [ - { version: 'NEXT', changes: [] } - ], - change_types, +export const base_changelog = { + project: { + name: null, + homepage: null, + with_stats: true, + ticket_url: null, + }, + releases: [{ version: "NEXT", changes: [] }], + change_types, }; const handler = async (config) => { - if( await fs.pathExists(config.source) ) { - consola.error(`${config.source} already exist, aborting.`); - process.exit(); - } + if (await fs.pathExists(config.source)) { + consola.error(`${config.source} already exist, aborting.`); + process.exit(); + } - consola.start(`creating ${config.source}...`); + consola.start(`creating ${config.source}...`); - await fs.writeFile( config.source, stringify(base_changelog) ); - - consola.success('done!'); + await fs.writeFile(config.source, stringify(base_changelog)); + consola.success("done!"); }; export default { - command: 'init', - desc : 'initialize new changelog source file', - builder: (yargs) => { - yargs.boolean('json') - .boolean('next') - .default('json',false) - .default('next',true); - }, - handler, -} + command: "init", + desc: "initialize new changelog source file", + builder: (yargs) => { + yargs + .boolean("json") + .boolean("next") + .default("json", false) + .default("next", true); + }, + handler, +}; diff --git a/src/command/print.js b/src/command/print.js index f90ee01..f741203 100644 --- a/src/command/print.js +++ b/src/command/print.js @@ -1,16 +1,131 @@ +import * as R from 'remeda'; +import fs from 'fs-extra'; +import yaml from 'yaml'; +import mkd from 'markdown-utils'; + +const render_header = (source ) => { + let header = '# Changelog'; + + let name = source.project?.name + + const links = {}; + + if( source?.project?.homepage) { + links.homepage = source?.project?.homepage; + name = `[${name}][homepage]`; + } + + if(name) header += ` for ${name}`; + + return { header, links } + +}; + +function render_links(links) { + + return Object.entries(links).map( + ([name,url]) => ` [${name}]: ${url}` + ).join( "\n") + +} + +const render_change = (config,source)=>(change) => { + let body = change.desc; + let links = {}; + + return { + body, links + } +}; + +export const render_release = (config,source) => ( release ) => { + let links = {}; + + if( typeof release === 'string') return { + body: release, + links + }; + + if(release.version === 'NEXT' && !config.next) return { + body: '', + links, + }; + + let body = "## " + release.version; + + if( release.date) { + body += " " + release.date; + } + + body += "\n\n"; + + const type_map = Object.fromEntries(source.change_types.flatMap( + entry => entry.keywords.map( k => [ k, entry ]) + )); + + const changes = release.changes.map( change => typeof change === 'string'? { desc: change, type: '' } : change ); + + const unknown = R.uniq( changes.map(R.prop('type')).map( + t => t ?? '' + ).filter( type => !type_map[type])); + + if( unknown.length ) { + throw new Error( + "unknown change types encountered: " + unknown.map(t => `'${t}'`).join(', ') + ); + } + + for ( const change_type of source.change_types ) { + const type_changes = changes.filter( + ({type}) => change_type.keywords.includes(type) + ); + + if( type_changes.length === 0) continue; + + if( change_type.title ) + body += mkd.h2( change_type.title) + "\n\n"; + + for( const c of type_changes ) { + const res = render_change(config,source)(c); + body += mkd.li(res.body,1)+"\n"; + links = { ...links, ...res.links}; + } + } + + return { body, links }; +}; + +const handler = async (config) => { + const source = await fs.readFile(config.source,'utf-8').then(yaml.parse); + + let output = ''; + + let {header,links} = render_header(source); + + output += header + "\n\n"; + + for ( const res of source.releases.map( render_release(config,source) )) { + output += res.body + "\n\n"; + links = {...links,...res.links}; + } + + output += "\n\n\n\n" + render_links(links); + + config.consola.raw(output); + -const handler = (...args) => { - console.log('hi!',args); }; export default { command: 'print', desc : 'render the changelog', builder: (yargs) => { - yargs.boolean('json') + yargs + //.boolean('json') .boolean('next') - .default('json',false) +// .default('json',false) .default('next',true); }, handler, } + diff --git a/src/command/print.test.js b/src/command/print.test.js new file mode 100644 index 0000000..301366e --- /dev/null +++ b/src/command/print.test.js @@ -0,0 +1,22 @@ +import { test, expect, vi } from 'vitest'; +import { render_release } from './print.js'; +import { base_changelog } from './init.js'; + +test( 'render_release a string', () => { + const {body} = render_release({})("as is"); + expect(body).toEqual('as is'); +}); + +test( 'render_release', () => { + const consola = { error: vi.fn() }; + const res= render_release({consola},base_changelog)({ + version: '1.2.3', + date: 'today', + changes: [ + "this", + "that", + ] + }); + expect(res.body).toContain('# 1.2.3 today'); + expect(res.body).toContain('* this'); +});