Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add monorepo support #45

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ npx changelogen@latest [...args] [<rootDir>]
- `--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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
36 changes: 8 additions & 28 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

124 changes: 82 additions & 42 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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<Chan
if (!config.output) {
config.output = false
} else if (config.output) {
config.output = config.output === true ? ConfigDefaults.output : resolve(cwd, config.output)
config.output = config.output === true ? ConfigDefaults.output : config.output
}

if (!config.github) {
5 changes: 3 additions & 2 deletions src/git.ts
Original file line number Diff line number Diff line change
@@ -43,9 +43,10 @@ export async function getCurrentGitRef () {
return await getCurrentGitTag() || await getCurrentGitBranch()
}

export async function getGitDiff (from: string | undefined, to: string = 'HEAD'): Promise<RawGitCommit[]> {
export async function getGitDiff (from: string | undefined, to: string = 'HEAD', dir?: string): Promise<RawGitCommit[]> {
// 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('|')
9 changes: 4 additions & 5 deletions src/semver.ts
Original file line number Diff line number Diff line change
@@ -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<string | false> {
export async function bumpVersion (commits: GitCommit[], config: ChangelogConfig, dir?: string): Promise<string | false> {
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