Skip to content
Merged
Show file tree
Hide file tree
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
32 changes: 32 additions & 0 deletions packages/core/src/project/monorepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from './monorepo.types.js'
import {
type ProjectOptions,
type ProjectRevertOptions,
Project,
bumpDefaultOptions
} from './project.js'
Expand Down Expand Up @@ -341,4 +342,35 @@ export abstract class MonorepoProject extends Project {

return this.independentBump(options)
}

override async revert(options: ProjectRevertOptions = {}) {
const {
manifest,
versionUpdates
} = this
const {
dryRun,
logger
} = options
const files = new Set<string>()
const rootUpdate = versionUpdates.find(({ files }) => files.includes(manifest.manifestPath))
let hasRevert = false

if (rootUpdate) {
logger?.verbose(`Reverting ${rootUpdate.name}: ${rootUpdate.to} -> ${rootUpdate.from}`)
rootUpdate.files.forEach(file => files.add(file))

await manifest.writeVersion(rootUpdate.from, dryRun)
hasRevert = true
}

for await (const project of this.getProjects()) {
hasRevert = await project.revert(options, files) || hasRevert
}

this.changedFiles = this.changedFiles.filter(file => !files.has(file))
this.versionUpdates.length = 0

return hasRevert
}
}
2 changes: 1 addition & 1 deletion packages/core/src/project/monorepo.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface MonorepoProjectOptions extends ProjectOptions {
gitClient?: ConventionalGitClient
}

export type MonorepoProjectBumpByProjectOptions = Pick<ProjectBumpOptions, 'version' | 'as' | 'prerelease' | 'firstRelease' | 'skip'>
export type MonorepoProjectBumpByProjectOptions = Pick<ProjectBumpOptions, 'version' | 'as' | 'prerelease' | 'snapshot' | 'skipChangelog' | 'firstRelease' | 'skip'>

export interface MonorepoProjectBumpOptions extends Omit<ProjectBumpOptions, 'tagPrefix'> {
/**
Expand Down
46 changes: 46 additions & 0 deletions packages/core/src/project/packageJson.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,31 @@ describe('core', () => {
expect(version).toBe('2.1.0-alpha.0')
})

it('should get next snapshot version from commits', async () => {
const { cwd } = await packageJsonProject()
const project = new PackageJsonProject({
path: join(cwd, 'package.json')
})
const version = await project.getNextVersion({
snapshot: 'snapshot'
})

expect(version).toMatch(/^2\.1\.0-snapshot\.\d{14}$/)
})

it('should get next snapshot version with given release type', async () => {
const { cwd } = await packageJsonProject()
const project = new PackageJsonProject({
path: join(cwd, 'package.json')
})
const version = await project.getNextVersion({
as: 'patch',
snapshot: 'canary'
})

expect(version).toMatch(/^2\.0\.1-canary\.\d{14}$/)
})

it('should dry bump version', async () => {
const { cwd } = await packageJsonProject()
const project = new PackageJsonProject({
Expand Down Expand Up @@ -199,6 +224,27 @@ describe('core', () => {
expect(await fs.readFile(join(cwd, 'CHANGELOG.md'), 'utf8')).toContain('Version bump without any changes.')
})

it('should skip changelog generation', async () => {
const { cwd } = await forkProject(
'bump-without-changelog',
packageJsonProject()
)
const changelogPath = join(cwd, 'CHANGELOG.md')
const changelog = await fs.readFile(changelogPath, 'utf8')
const project = new PackageJsonProject({
path: join(cwd, 'package.json')
})
const result = await project.bump({
as: 'patch',
skipChangelog: true
})

expect(result).toBe(true)
expect(project.changedFiles).toMatchObject([expect.stringMatching(/package\.json$/)])
expect(project.versionUpdates[0].notes).toBe('')
expect(await fs.readFile(changelogPath, 'utf8')).toBe(changelog)
})

it('should not add placeholder when generated release notes are not empty', async () => {
const { cwd } = await packageJsonProject()
const project = new PackageJsonProject({
Expand Down
101 changes: 75 additions & 26 deletions packages/core/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ import {
extractLastReleaseFromFile,
preamblePartial
} from '../change-log.js'
import { getReleaseType } from '../utils.js'
import {
getPrereleaseIdentifier,
getPrereleaseIdentifierBase,
getReleaseType
} from '../utils.js'
import type {
ProjectOptions,
ProjectBumpOptions,
ProjectVersionUpdate,
ProjectTagsOptions,
ProjectReleaseOptions,
ProjectPublishOptions
ProjectPublishOptions,
ProjectRevertOptions
} from './project.types.js'

export type * from './project.types.js'
Expand Down Expand Up @@ -190,6 +195,7 @@ export abstract class Project {
baseVersion,
as,
prerelease,
snapshot,
firstRelease: firstReleaseOption,
tagPrefix,
preset = bumpDefaultOptions.preset
Expand Down Expand Up @@ -237,10 +243,15 @@ export abstract class Project {
return null
}

const prereleaseIdentifier = getPrereleaseIdentifier(
prerelease,
snapshot
)
const nextVersion = semver.inc(
version,
getReleaseType(releaseType, version, prerelease),
prerelease
getReleaseType(releaseType, version, prereleaseIdentifier),
prereleaseIdentifier,
getPrereleaseIdentifierBase(snapshot)
)

return nextVersion
Expand All @@ -267,6 +278,7 @@ export abstract class Project {
tagPrefix,
preset = bumpDefaultOptions.preset,
dryRun,
skipChangelog,
logger
} = options
const { projectPath } = manifest
Expand All @@ -276,6 +288,7 @@ export abstract class Project {
notes: ''
}

this.versionUpdates.push(versionUpdate)
this.changedFiles.push(...versionUpdate.files)

Comment thread
dangreen marked this conversation as resolved.
const changelogPath = join(projectPath, changelogFile!)
Expand All @@ -287,31 +300,67 @@ export abstract class Project {
logger?.verbose(`${name}: ${version} -> ${nextVersion}`)
}

const notes = new ConventionalChangelog(gitClient)
.loadPreset(preset, _ => import(_))
.commits({
path: projectPath
})
.tags({
prefix: tagPrefix
})
.readRepository()
.context({
version: nextVersion
})
.writer({
preamblePartial
})
.write()
if (!skipChangelog) {
const notes = new ConventionalChangelog(gitClient)
.loadPreset(preset, _ => import(_))
.commits({
path: projectPath
})
.tags({
prefix: tagPrefix
})
.readRepository()
.context({
version: nextVersion
})
.writer({
preamblePartial
})
.write()

versionUpdate.notes = dryRun
? await concatStringStream(notes)
: await addReleaseNotes(changelogPath, notes)
versionUpdate.notes = dryRun
? await concatStringStream(notes)
: await addReleaseNotes(changelogPath, notes)

logger?.verbose(`Release notes:\n\n${versionUpdate.notes}`)
logger?.verbose(`Release notes:\n\n${versionUpdate.notes}`)

this.changedFiles.push(changelogPath)
this.versionUpdates.push(versionUpdate)
this.changedFiles.push(changelogPath)
}

return true
}

/**
* Revert version updates.
* @param options - The options to use for reverting.
* @returns Whether version updates were reverted.
*/
async revert(
options: ProjectRevertOptions = {},
filesSet: Set<string> = new Set()
) {
const {
manifest,
versionUpdates
} = this
const {
dryRun,
logger
} = options

if (!versionUpdates.length) {
return false
}

for (const update of versionUpdates) {
logger?.verbose(`Reverting ${update.name}: ${update.to} -> ${update.from}`)
update.files.forEach(file => filesSet.add(file))

await manifest.writeVersion(update.from, dryRun)
}
Comment thread
dangreen marked this conversation as resolved.

this.changedFiles = this.changedFiles.filter(file => !filesSet.has(file))
this.versionUpdates.length = 0

return true
}
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/project/project.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ export interface ProjectBumpOptions {
* The pre-release identifier to use.
*/
prerelease?: string
/**
* The snapshot pre-release identifier to use with timestamp suffix.
*/
snapshot?: string
/**
* Skip changelog generation.
*/
skipChangelog?: boolean
/**
* Whether this is the first release.
* By default will be auto detected based on tag existence.
Expand Down Expand Up @@ -108,4 +116,17 @@ export interface ProjectPublishOptions {
* Skip publishing.
*/
skip?: boolean
/**
* The NPM tag to use for publishing.
*/
tag?: string | ((version: string, prerelease: readonly (string | number)[] | null) => string)
/**
* Whether to perform git checks before publishing.
*/
gitChecks?: boolean
}

export interface ProjectRevertOptions {
dryRun?: boolean
logger?: ChildLogger
}
33 changes: 32 additions & 1 deletion packages/core/src/releaser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
expect
} from 'vitest'
import { firstFromStream } from '@simple-libs/stream-utils'
import { packageJsonProject } from 'test'
import {
forkProject,
packageJsonProject
} from 'test'
import { PackageJsonProject } from './project/packageJson.js'
import { Releaser } from './releaser.js'

Expand Down Expand Up @@ -41,5 +44,33 @@ describe('core', () => {
expect(tag).toBe('v2.1.0')
expect(packageJson.version).toBe('2.1.0')
})

it('should revert version updates', async () => {
const { cwd } = await forkProject('revert', packageJsonProject({
name: 'revert-package-json-project'
}))
const project = new PackageJsonProject({
path: join(cwd, 'package.json')
})
const releaser = new Releaser({
project,
silent: true
})
.bump({
as: 'patch',
skipChangelog: true
})
.revert()

await releaser.run()

const packageJson = JSON.parse(
await fs.readFile(join(cwd, 'package.json'), 'utf-8')
)

expect(packageJson.version).toBe('2.0.0')
expect(project.changedFiles).toEqual([])
expect(project.versionUpdates).toEqual([])
})
})
})
28 changes: 28 additions & 0 deletions packages/core/src/releaser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,34 @@ export class Releaser<
})
}

/**
* Enqueue a task to revert version update files.
* @returns Project releaser instance for chaining.
*/
revert() {
return this.enqueue(async () => {
const {
project,
logger
} = this
const { dryRun } = this.options

logger.info('revert', 'Reverting version updates...')

const done = await project.revert({
dryRun,
logger: logger.createChild('revert')
})

if (!done) {
logger.info('revert', 'No version updates to revert.')
return
}

this.state.bump = false
})
}

/**
* Enqueue a task to tag the project with the new version.
* @param options
Expand Down
Loading