From 1d8e408ab6059be09e248262f776f05ba1507ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Coletta?= Date: Fri, 21 Oct 2022 18:27:47 +0200 Subject: [PATCH] feat: Add monorepo support --- README.md | 1 + package.json | 1 + pnpm-lock.yaml | 36 ++++---------- src/cli.ts | 124 ++++++++++++++++++++++++++++++++----------------- src/config.ts | 4 +- src/git.ts | 5 +- src/semver.ts | 9 ++-- 7 files changed, 101 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index e67c819..aa24df4 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ npx changelogen@latest [...args] [] - `--bump`: Determine semver change and update version in `package.json`. - `--release`. Bumps version in `package.json` and creates commit and git tags using local `git`. You can disable commit using `--no-commit` and tag using `--no-tag`. - `-r`: Release as specific version. +- `--recursive`: Generate a CHANGELOG in every dir that have a package.json file. You can optionnaly specify a glob pattern to packages.json files (e.g: `changelogen --recursive packages/**/package.json`) ## Configuration diff --git a/package.json b/package.json index 3e0329e..41b802f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "consola": "^2.15.3", "convert-gitmoji": "^0.1.2", "execa": "^6.1.0", + "fast-glob": "^3.2.12", "mri": "^1.2.0", "pkg-types": "^0.3.5", "scule": "^0.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af960cd..e6a5fa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,7 @@ specifiers: convert-gitmoji: ^0.1.2 eslint: ^8.25.0 execa: ^6.1.0 + fast-glob: ^3.2.12 jiti: ^1.16.0 mri: ^1.2.0 pkg-types: ^0.3.5 @@ -25,6 +26,7 @@ dependencies: consola: 2.15.3 convert-gitmoji: 0.1.2 execa: 6.1.0 + fast-glob: 3.2.12 mri: 1.2.0 pkg-types: 0.3.5 scule: 0.3.2 @@ -372,12 +374,10 @@ packages: dependencies: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 - dev: true /@nodelib/fs.stat/2.0.5: resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} - dev: true /@nodelib/fs.walk/1.2.8: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} @@ -385,7 +385,6 @@ packages: dependencies: '@nodelib/fs.scandir': 2.1.5 fastq: 1.13.0 - dev: true /@nuxtjs/eslint-config-typescript/11.0.0_z4bbprzjrhnsfa24uvmcbu7f5q: resolution: {integrity: sha512-hmFjGtXT524ql8eTbK8BaRkamcXB6Z8YOW8nSQhosTP6oBw9WtOFUeWr7holyE278UhOmx+wDFG90BnyM9D+UA==} @@ -848,7 +847,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 - dev: true /browserslist/4.20.3: resolution: {integrity: sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==} @@ -885,7 +883,7 @@ packages: gittar: 0.1.1 jiti: 1.16.0 mlly: 0.5.14 - pathe: 0.3.8 + pathe: 0.3.9 pkg-types: 0.3.5 rc9: 1.2.2 dev: false @@ -2244,8 +2242,8 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true - /fast-glob/3.2.11: - resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} + /fast-glob/3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2253,7 +2251,6 @@ packages: glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 - dev: true /fast-json-stable-stringify/2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2267,7 +2264,6 @@ packages: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} dependencies: reusify: 1.0.4 - dev: true /figures/3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} @@ -2288,7 +2284,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /find-up/2.1.0: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} @@ -2478,7 +2473,6 @@ packages: engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 - dev: true /glob-parent/6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -2531,7 +2525,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.11 + fast-glob: 3.2.12 ignore: 5.2.0 merge2: 1.4.1 slash: 3.0.0 @@ -2542,7 +2536,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.2.11 + fast-glob: 3.2.12 ignore: 5.2.0 merge2: 1.4.1 slash: 4.0.0 @@ -2740,7 +2734,6 @@ packages: /is-extglob/2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - dev: true /is-fullwidth-code-point/3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} @@ -2752,7 +2745,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 - dev: true /is-module/1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} @@ -2773,7 +2765,6 @@ packages: /is-number/7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true /is-obj/2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} @@ -3081,7 +3072,6 @@ packages: /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - dev: true /micromatch/4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} @@ -3089,7 +3079,6 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 - dev: true /mimic-fn/4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} @@ -3175,7 +3164,7 @@ packages: resolution: {integrity: sha512-DgRgNUSX9NIxxCxygX4Xeg9C7GX7OUx1wuQ8cXx9o9LE0e9wrH+OZ9fcnrlEedsC/rtqry3ZhUddC759XD/L0w==} dependencies: acorn: 8.8.0 - pathe: 0.3.8 + pathe: 0.3.9 pkg-types: 0.3.5 ufo: 0.8.5 dev: false @@ -3449,10 +3438,6 @@ packages: resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} dev: true - /pathe/0.3.8: - resolution: {integrity: sha512-c71n61F1skhj/jzZe+fWE9XDoTYjWbUwIKVwFftZ5IOgiX44BVkTkD+/803YDgR50tqeO4eXWxLyVHBLWQAD1g==} - dev: false - /pathe/0.3.9: resolution: {integrity: sha512-6Y6s0vT112P3jD8dGfuS6r+lpa0qqNrLyHPOwvXMnyNTQaYiwgau2DP3aNDsR13xqtGj7rrPo+jFUATpU6/s+g==} @@ -3467,7 +3452,6 @@ packages: /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true /pify/2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} @@ -3534,7 +3518,6 @@ packages: /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true /quick-lru/4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} @@ -3646,7 +3629,6 @@ packages: /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} @@ -3689,7 +3671,6 @@ packages: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 - dev: true /safe-buffer/5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -4009,7 +3990,6 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 - dev: true /trim-newlines/3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} diff --git a/src/cli.ts b/src/cli.ts index 2f9e614..c344ca6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,9 +1,10 @@ #!/usr/bin/env node -import { resolve } from 'path' +import { resolve, dirname, join } from 'path' import { existsSync, promises as fsp } from 'fs' import consola from 'consola' import mri from 'mri' import { execa } from 'execa' +import fg from 'fast-glob' import { getGitDiff, parseCommits } from './git' import { loadChangelogConfig } from './config' import { generateMarkDown } from './markdown' @@ -18,62 +19,101 @@ async function main () { from: args.from, to: args.to, output: args.output, - newVersion: args.r + newVersion: args.r, + recursive: args.recursive }) const logger = consola.create({ stdout: process.stderr }) logger.info(`Generating changelog for ${config.from}...${config.to}`) - const rawCommits = await getGitDiff(config.from, config.to) + const packages = ['package.json'] - // Parse commits as conventional commits - const commits = parseCommits(rawCommits, config).filter(c => - config.types[c.type] && - !(c.type === 'chore' && c.scope === 'deps' && !c.isBreaking) - ) - - // Bump version optionally - if (args.bump || args.release) { - const newVersion = await bumpVersion(commits, config) - if (!newVersion) { - consola.error('Unable to bump version based on changes.') - process.exit(1) - } - config.newVersion = newVersion + if (config.recursive) { + const globPattern = config.recursive === true ? '**/package.json' : config.recursive + let recursivePackagesJson = await fg(globPattern, { + cwd: config.cwd, + ignore: ['**/node_modules'] + }) + // Remove root package.json since we already have it by default + recursivePackagesJson = recursivePackagesJson.filter(packageJsonLocation => packageJsonLocation !== 'package.json') + packages.push(...recursivePackagesJson) + logger.info(`The following packages were detected : \n - ${packages.join('\n - ')}`) } - // Generate markdown - const markdown = generateMarkDown(commits, config) + for (const packageLocation of packages) { + const packageLocationDir = dirname(packageLocation) + const rawCommits = await getGitDiff(config.from, config.to, packageLocationDir) - // Show changelog in CLI unless bumping or releasing - const displayOnly = !args.bump && !args.release - if (displayOnly) { - consola.log('\n\n' + markdown + '\n\n') - } + // Parse commits as conventional commits + const commits = parseCommits(rawCommits, config).filter(c => + config.types[c.type] && + !(c.type === 'chore' && c.scope === 'deps' && !c.isBreaking) + ) + + // Bump version optionally + const isRootPackage = packageLocation === 'package.json' + if (args.bump || args.release) { + const newVersion = await bumpVersion(commits, config, packageLocationDir) + + if (isRootPackage) { + if (!newVersion) { + consola.error('Unable to bump version based on changes.') + process.exit(1) + } else { + config.newVersion = newVersion + } + } - // Update changelog file (only when bumping or releasing or when --output is specified as a file) - if (typeof config.output === 'string' && (args.output || !displayOnly)) { - let changelogMD: string - if (existsSync(config.output)) { - consola.info(`Updating ${config.output}`) - changelogMD = await fsp.readFile(config.output, 'utf8') - } else { - consola.info(`Creating ${config.output}`) - changelogMD = '# Changelog\n\n' + // Skip package if no new version + if (!newVersion) { + logger.info(`No bump required for package '${packageLocation}'`) + continue + } } - const lastEntry = changelogMD.match(/^###?\s+.*$/m) + // Generate markdown + const markdown = generateMarkDown(commits, config) - if (lastEntry) { - changelogMD = - changelogMD.slice(0, lastEntry.index) + - markdown + '\n\n' + - changelogMD.slice(lastEntry.index) - } else { - changelogMD += '\n' + markdown + '\n\n' + // Show changelog in CLI unless bumping or releasing + const displayOnly = !args.bump && !args.release + if (displayOnly) { + if (!config.recursive) { + consola.log('\n\n' + markdown + '\n\n') + } else { + consola.log(packageLocationDir + ' : \n\n' + markdown + '\n\n') + } } - await fsp.writeFile(config.output, changelogMD) + // Update changelog file (only when bumping or releasing or when --output is specified as a file) + const changelogOutputPath = config.output + if (typeof changelogOutputPath === 'string' && (args.output || !displayOnly)) { + let changelogMD: string + const changelogPath = config.recursive ? join(packageLocationDir, changelogOutputPath) : changelogOutputPath + if (existsSync(changelogPath)) { + consola.info(`Updating ${changelogPath}`) + changelogMD = await fsp.readFile(changelogPath, 'utf8') + } else { + consola.info(`Creating ${changelogPath}`) + changelogMD = '# Changelog\n\n' + } + + const lastEntry = changelogMD.match(/^###?\s+.*$/m) + + if (lastEntry) { + changelogMD = + changelogMD.slice(0, lastEntry.index) + + markdown + '\n\n' + + changelogMD.slice(lastEntry.index) + } else { + changelogMD += '\n' + markdown + '\n\n' + } + + await fsp.writeFile(changelogPath, changelogMD) + // Stage the file in release mode + if (args.release && args.commit !== false) { + await execa('git', ['add', changelogPath], { cwd }) + } + } } // Commit and tag changes for release mode diff --git a/src/config.ts b/src/config.ts index b074b81..111db5b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,3 @@ -import { resolve } from 'path' import { loadConfig } from 'c12' import { readPackageJSON } from 'pkg-types' import { getLastGitTag, getCurrentGitRef } from './git' @@ -13,6 +12,7 @@ export interface ChangelogConfig { to: string newVersion?: string output: string | boolean + recursive?: string | boolean } const ConfigDefaults: ChangelogConfig = { @@ -60,7 +60,7 @@ export async function loadChangelogConfig (cwd: string, overrides?: Partial { +export async function getGitDiff (from: string | undefined, to: string = 'HEAD', dir?: string): Promise { // https://git-scm.com/docs/pretty-formats - const r = await execCommand('git', ['--no-pager', 'log', `${from ? `${from}...` : ''}${to}`, '--pretty="----%n%s|%h|%an|%ae%n%b"', '--name-status']) + const dirArgs = dir ? ['--', dir] : [] + const r = await execCommand('git', ['--no-pager', 'log', `${from ? `${from}...` : ''}${to}`, '--pretty="----%n%s|%h|%an|%ae%n%b"', '--name-status', ...dirArgs]) return r.split('----\n').splice(1).map((line) => { const [firstLine, ..._body] = line.split('\n') const [message, shortHash, authorName, authorEmail] = firstLine.split('|') diff --git a/src/semver.ts b/src/semver.ts index 75f13af..70b880e 100644 --- a/src/semver.ts +++ b/src/semver.ts @@ -23,11 +23,11 @@ export function determineSemverChange (commits: GitCommit[], config: ChangelogCo return hasMajor ? 'major' : (hasMinor ? 'minor' : (hasPatch ? 'patch' : null)) } -export async function bumpVersion (commits: GitCommit[], config: ChangelogConfig): Promise { +export async function bumpVersion (commits: GitCommit[], config: ChangelogConfig, dir?: string): Promise { let type = determineSemverChange(commits, config) const originalType = type - const pkgPath = resolve(config.cwd, 'package.json') + const pkgPath = resolve(config.cwd, dir, 'package.json') const pkg = JSON.parse(await fsp.readFile(pkgPath, 'utf8').catch(() => '{}')) || {} const currentVersion = pkg.version || '0.0.0' @@ -39,19 +39,18 @@ export async function bumpVersion (commits: GitCommit[], config: ChangelogConfig } } - if (config.newVersion) { + if ((!config.recursive || !dir || dir === '.') && config.newVersion) { pkg.version = config.newVersion } else if (type) { // eslint-disable-next-line import/no-named-as-default-member pkg.version = semver.inc(currentVersion, type) - config.newVersion = pkg.version } if (pkg.version === currentVersion) { return false } - consola.info(`Bumping version from ${currentVersion} to ${pkg.version} (${originalType})`) + consola.info(`Bumping version ${dir && dir !== '.' ? `in ${dir} ` : ''}from ${currentVersion} to ${pkg.version} (${originalType})`) await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8') return pkg.version