UNPKG

@release-it-plugins/lerna-changelog

Version:
216 lines (166 loc) 5.99 kB
import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { createRequire } from 'module'; import { EOL } from 'os'; import fs from 'fs'; import which from 'which'; import { Plugin } from 'release-it'; import _ from 'lodash'; import tmp from 'tmp'; import execa from 'execa'; import { fromMarkdown } from 'mdast-util-from-markdown'; const template = _.template; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); import validatePeerDependencies from 'validate-peer-dependencies'; validatePeerDependencies(__dirname); const require = createRequire(import.meta.url); const LERNA_PATH = require.resolve('lerna-changelog/bin/cli'); // using a const here, because we may need to change this value in the future // and this makes it much simpler const UNRELEASED = 'Unreleased'; function getToday() { const date = new Date().toISOString(); return date.slice(0, date.indexOf('T')); } export default class LernaChangelogGeneratorPlugin extends Plugin { async init() { let from = (await this.getTagForHEAD()) || (await this.getFirstCommit()); this.changelog = await this._execLernaChangelog(from); // this supports release-it < 13.5.3 this.setContext({ changelog: this.changelog }); } get nextVersion() { let { version } = this.config.getContext(); let tagName = this.config.getContext('git.tagName'); let nextVersion = tagName ? template(tagName)({ version }) : version; return nextVersion; } // this hook is supported by release-it@13.5.5+ getChangelog() { return this.changelog; } async getTagForHEAD() { try { return await this.exec('git describe --tags --abbrev=0', { options: { write: false } }); } catch (error) { return null; } } async getFirstCommit() { if (this._firstCommit) { return this._firstCommit; } this._firstCommit = await this.exec(`git rev-list --max-parents=0 HEAD`, { options: { write: false }, }); return this._firstCommit; } async _execLernaChangelog(from) { let changelog = await this.exec( `${process.execPath} ${LERNA_PATH} --next-version=${UNRELEASED} --from=${from}`, { options: { write: false }, } ); return changelog; } async processChangelog() { // this is populated in `init` let changelog = this.changelog ? this.changelog.replace(UNRELEASED, this.nextVersion) : `## ${this.nextVersion} (${getToday()})`; let finalChangelog = await this.reviewChangelog(changelog); return finalChangelog; } async _launchEditor(tmpFile) { // do not launch the editor for dry runs if (this.config.isDryRun) { return; } let editorCommand; if (typeof this.options.launchEditor === 'boolean') { let EDITOR = process.env.EDITOR; if (!EDITOR) { EDITOR = which.sync('editor', { nothrow: true }); } if (!EDITOR) { let error = new Error( `@release-it-plugins/lerna-changelog configured to launch your editor but no editor was found (tried $EDITOR and searching $PATH for \`editor\`).` ); this.log.error(error.message); throw error; } // `${file}` is interpolated just below editorCommand = EDITOR + ' ${file}'; } else { editorCommand = this.options.launchEditor; } editorCommand = editorCommand.replace('${file}', tmpFile); await execa.command(editorCommand, { stdio: 'inherit' }); } async reviewChangelog(changelog) { if (!this.options.launchEditor) { return changelog; } let tmpFile = tmp.fileSync().name; fs.writeFileSync(tmpFile, changelog, { encoding: 'utf-8' }); await this._launchEditor(tmpFile); let finalChangelog = fs.readFileSync(tmpFile, { encoding: 'utf-8' }); return finalChangelog; } async writeChangelog(changelog) { const { infile } = this.options; let hasInfile = false; try { fs.accessSync(infile); hasInfile = true; } catch (err) { this.debug(err); } if (!hasInfile) { // generate an initial CHANGELOG.md with all of the versions let firstCommit = await this.getFirstCommit(); if (firstCommit) { changelog = await this._execLernaChangelog(firstCommit, this.nextVersion); changelog = changelog.replace(UNRELEASED, this.nextVersion); this.debug({ changelog }); } else { // do something when there is no commit? not sure what our options are... } } if (this.config.isDryRun) { this.log.log(`! Prepending ${infile} with release notes.`); } else { let currentFileData = hasInfile ? fs.readFileSync(infile, { encoding: 'utf8' }) : ''; let newContent = this._insertContent(changelog, currentFileData); fs.writeFileSync(infile, newContent, { encoding: 'utf8' }); } if (!hasInfile) { await this.exec(`git add ${infile}`); } } _insertContent(newContent, oldContent) { let insertOffset = this._findInsertOffset(oldContent); let before = oldContent.slice(0, insertOffset); let after = oldContent.slice(insertOffset); return before + newContent + EOL + EOL + after; } _findInsertOffset(oldContent) { let ast = fromMarkdown(oldContent); let firstH2 = ast.children.find((it) => it.type === 'heading' && it.depth === 2); return firstH2 ? firstH2.position.start.offset : 0; } async beforeRelease() { let processedChangelog = await this.processChangelog(); this.debug({ changelog: processedChangelog }); // remove first two lines to prevent release notes // from including the version number/date (it looks odd // in the Github/Gitlab UIs) let changelogWithoutVersion = processedChangelog.split(EOL).slice(2).join(EOL); this.config.setContext({ changelog: changelogWithoutVersion }); if (this.options.infile) { await this.writeChangelog(processedChangelog); } } }