Skip to content

Commit 04ce14b

Browse files
committed
[WIP] Extract code to validate a version bump
1 parent 4e4dab0 commit 04ce14b

2 files changed

Lines changed: 123 additions & 107 deletions

File tree

‎packages/releases/bin/update-changelog.js‎

Lines changed: 46 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
#!/usr/bin/env node
22

33
const { resolve } = require('node:path')
4-
const { writeFileSync } = require('node:fs')
4+
const { writeFileSync, readFileSync } = require('node:fs')
55

6-
const semver = require('semver')
7-
8-
const { readFileLinesSync, versionIsAPrerelease, getPrereleaseIdentifier, getChangelogLineIndexes } = require('../changelog-release-helper.js')
6+
const { getChangelogLineIndexes } = require('../changelog-release-helper.js')
97

108
const config = require('../config.js')
9+
const { VersionBump } = require('../version-bump.js')
1110

1211
if (require.main === module) {
1312
// npm exposes these environment variable as part of the lifecycle hooks
1413
// (https://github.com/npm/cli/blob/c97b39b1e3436cd20a67ab5f4012a5f395c538b9/workspaces/libnpmversion/lib/version.js#L100-L103)
1514
const { npm_old_version: previousVersion, npm_new_version: newVersion } =
1615
process.env
1716

18-
if (!previousVersion || !newVersion) {
19-
throw new Error('Both previous and new version must be set to continue.')
20-
}
17+
const bump = new VersionBump(newVersion, previousVersion)
2118

2219
console.log(
2320
`Updating changelog from version ${previousVersion} to ${newVersion}...`
@@ -26,7 +23,7 @@ if (require.main === module) {
2623
// Use `resolve` to find the CHANGELOG.md file relative to the current working directory
2724
const changelogPath = resolve(process.argv[2] ?? 'CHANGELOG.md')
2825

29-
updateChangelog(changelogPath, newVersion, previousVersion)
26+
updateChangelog(changelogPath, bump)
3027
}
3128

3229
/**
@@ -36,118 +33,60 @@ if (require.main === module) {
3633
* content
3734
*
3835
* @param {string} path - Path to the CHANGELOG file
39-
* @param {string} newVersion - New version to add to the changelog
40-
* @param {string} previousVersion - Previous version. Used for calculating difference
41-
* in versions to build the changelog title
36+
* @param {VersionBump} bump - New version to add to the changelog
4237
*/
43-
function updateChangelog (path, newVersion, previousVersion) {
44-
const validatedNewVersion = validateVersionNumber(newVersion)
45-
const validatedPreviousVersion = validateVersionNumber(previousVersion)
46-
47-
// Skip the entire function if the release version is internal eg: 5.1.0-internal.0
48-
const newVersionIsAPrerelease = versionIsAPrerelease(validatedNewVersion)
49-
if (newVersionIsAPrerelease) {
50-
const identifier = getPrereleaseIdentifier(validatedNewVersion)
51-
52-
if (identifier === 'internal') {
53-
console.log(
54-
'This is an internal release, intended for testing only. The changelog will therefore not be updated.'
55-
)
56-
return
57-
}
58-
}
59-
60-
const changelogLines = readFileLinesSync(path)
61-
const [startIndex] = getChangelogLineIndexes(changelogLines)
38+
function updateChangelog (path, bump) {
6239

63-
const versionDiff = semver.diff(validatedNewVersion, validatedPreviousVersion)
64-
if (!versionDiff) {
65-
throw new Error(`New version (${validatedNewVersion}) and previous version (${validatedPreviousVersion}) are the same`)
66-
}
67-
const newVersionTitle = `## ${validatedNewVersion} (${capitalise(convertIncTypeWord(versionDiff, validatedNewVersion))})`
68-
69-
const newLines = [newVersionTitle, '']
70-
if (newVersionIsAPrerelease) {
71-
newLines.push(
72-
config.prereleaseWarning(removePrereleaseFlag(validatedNewVersion)),
73-
''
74-
)
75-
// Add content for installing pre-releases
76-
newLines.push(
77-
config.prereleaseInstallationInstructions(validatedNewVersion),
78-
''
79-
)
80-
} else {
81-
// Add content on how to install the release
82-
newLines.push(
83-
config.installationInstructions(),
84-
''
85-
)
40+
if (!bump.needsChangelogUpdate) {
41+
console.log('This is an internal release, intended for testing only. The changelog will therefore not be updated.')
42+
return
8643
}
8744

88-
// Inject the new lines into the CHANGELOG
89-
changelogLines.splice(startIndex + 1, 0, '', ...newLines)
45+
updateFile(path, (content) => {
46+
const changelogLines = content.split('\n');
9047

91-
writeFileSync(path, changelogLines.join('\n'))
92-
}
93-
94-
/**
95-
* Validates the version number that it is a semantic versioned string.
96-
*
97-
* @param {string} version - version number
98-
* @returns {string} - Validated semver of version
99-
*/
100-
function validateVersionNumber (version) {
101-
const validatedVersion = semver.valid(version)
48+
const [startIndex] = getChangelogLineIndexes(changelogLines)
10249

103-
if (!validatedVersion) {
104-
throw new Error(
105-
`Version number "${version}" could not be parsed as a semantic versioned string.`
106-
)
107-
}
50+
const newVersionTitle = `## ${bump.newVersion} (${capitalise(bump.releaseLabel)})`
10851

109-
return validatedVersion
110-
}
111-
112-
/**
113-
* Convert a standard SemVer increment word eg: major, minor or patch into the
114-
* wording we use for release titles.
115-
*
116-
* @param {string} incType - SemVer increment type
117-
* @param {string|null} version - SemVer version
118-
* @returns {string} - The reworded increment type
119-
*/
120-
function convertIncTypeWord (incType, version) {
121-
let rewordedIncType = incType
122-
123-
// If there's a prerelease flag e.g. 1.0.0-beta.0 use that to decide
124-
const prereleaseIdentifier = getPrereleaseIdentifier(version)
125-
if (prereleaseIdentifier) {
126-
if (prereleaseIdentifier === 'rc') {
127-
return 'release candidate'
52+
const newLines = [newVersionTitle, '']
53+
if (bump.toPrerelease) {
54+
newLines.push(
55+
config.prereleaseWarning(bump.withoutPrereleaseTag),
56+
''
57+
)
58+
// Add content for installing pre-releases
59+
newLines.push(
60+
config.prereleaseInstallationInstructions(bump.newVersion),
61+
''
62+
)
63+
} else {
64+
// Add content on how to install the release
65+
newLines.push(
66+
config.installationInstructions(),
67+
''
68+
)
12869
}
129-
rewordedIncType = prereleaseIdentifier
130-
} else if (incType === 'major') {
131-
rewordedIncType = 'breaking'
132-
} else if (incType === 'minor') {
133-
rewordedIncType = 'feature'
134-
} else if (incType === 'patch') {
135-
rewordedIncType = 'fix'
136-
}
13770

138-
return `${rewordedIncType} release`
71+
// Inject the new lines into the CHANGELOG
72+
changelogLines.splice(startIndex + 1, 0, '', ...newLines)
73+
74+
return changelogLines.join('\n');
75+
})
13976
}
14077

14178
/**
142-
* Remove any pre-release flag from a version e.g. 1.0.0-alpha -> 1.0.0
143-
*
144-
* @param {string} version - version number
145-
* @returns {string} - version number without any pre-release flag
79+
* Updates file at given path with the result from the given function
80+
*
81+
* @param {string} path - The path of the file to update
82+
* @param {string => string | null | undefined} callback - The function updating the content
14683
*/
147-
function removePrereleaseFlag (version) {
148-
const parsedVersion = semver.parse(version)
149-
parsedVersion.prerelease = []
150-
return parsedVersion.format()
84+
function updateFile(path, callback) {
85+
const content = readFileSync(path, {encoding: 'utf-8'});
86+
const updatedContent = callback(content);
87+
if (updatedContent) {
88+
writeFileSync(path, content)
89+
}
15190
}
15291

15392
/**

‎packages/releases/version-bump.js‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const semver = require('semver')
2+
3+
const {versionIsAPrerelease, getPrereleaseIdentifier } = require('./changelog-release-helper.js');
4+
5+
class VersionBump {
6+
constructor(newVersion, previousVersion) {
7+
8+
if (!previousVersion || !newVersion) {
9+
throw new Error('Both previous and new version must be set to continue.')
10+
}
11+
12+
this.newVersion = validateVersionNumber(newVersion)
13+
this.previousVersion = validateVersionNumber(previousVersion)
14+
this.toPrerelease = versionIsAPrerelease(this.newVersion);
15+
16+
if (this.toPrerelease) {
17+
const identifier = getPrereleaseIdentifier(this.newVersion)
18+
19+
this.needsChangelogUpdate = identifier === 'internal'
20+
}
21+
22+
this.versionDiff = semver.diff(this.newVersion, this.previousVersion)
23+
24+
if (!this.versionDiff) {
25+
throw new Error(`New version (${this.newVersion}) and previous version (${this.previousVersion}) are the same`)
26+
}
27+
}
28+
29+
get releaseLabel() {
30+
let rewordedIncType = this.versionDiff
31+
32+
// If there's a prerelease flag e.g. 1.0.0-beta.0 use that to decide
33+
const prereleaseIdentifier = getPrereleaseIdentifier(this.newVersion)
34+
if (prereleaseIdentifier) {
35+
if (prereleaseIdentifier === 'rc') {
36+
return 'release candidate'
37+
}
38+
rewordedIncType = prereleaseIdentifier
39+
} else if (incType === 'major') {
40+
rewordedIncType = 'breaking'
41+
} else if (incType === 'minor') {
42+
rewordedIncType = 'feature'
43+
} else if (incType === 'patch') {
44+
rewordedIncType = 'fix'
45+
}
46+
47+
return `${rewordedIncType} release`
48+
}
49+
50+
get withoutPrereleaseTag() {
51+
const parsedVersion = semver.parse(this.newVersion)
52+
parsedVersion.prerelease = []
53+
return parsedVersion.format()
54+
}
55+
}
56+
57+
/**
58+
* Validates the version number that it is a semantic versioned string.
59+
*
60+
* @param {string} version - version number
61+
* @returns {string} - Validated semver of version
62+
*/
63+
function validateVersionNumber (version) {
64+
const validatedVersion = semver.valid(version)
65+
66+
if (!validatedVersion) {
67+
throw new Error(
68+
`Version number "${version}" could not be parsed as a semantic versioned string.`
69+
)
70+
}
71+
72+
return validatedVersion
73+
}
74+
75+
module.exports = {
76+
VersionBump
77+
}

0 commit comments

Comments
 (0)