From 3106982df3ddcf48f89d236f97f2a745e02e0743 Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Wed, 29 Apr 2026 22:22:21 -0500 Subject: [PATCH 1/9] Create "truncate" function that handles patch, minor, and major --- functions/truncate.js | 36 ++++++++++++++++++++++++++++++++++++ test/fixtures/truncations.js | 14 ++++++++++++++ test/functions/truncate.js | 20 ++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 functions/truncate.js create mode 100644 test/fixtures/truncations.js create mode 100644 test/functions/truncate.js diff --git a/functions/truncate.js b/functions/truncate.js new file mode 100644 index 00000000..563c76f7 --- /dev/null +++ b/functions/truncate.js @@ -0,0 +1,36 @@ +'use strict' + +const SemVer = require('../classes/semver') +const constants = require('../internal/constants') + +const truncate = (version, releaseType, options) => { + // if (typeof (options) === 'string') { + // options = undefined + // } + + if (!constants.RELEASE_TYPES.includes(releaseType)) { + return null + } + + try { + const currentVersion = new SemVer( + version instanceof SemVer ? version.version : version, + options + ) + + currentVersion.prerelease = [] + + switch (releaseType) { + case 'major': + currentVersion.minor = 0 + // eslint-disable-next-line no-fallthrough -- this is intentional fallthrough + case 'minor': + currentVersion.patch = 0 + } + + return currentVersion.format() + } catch (er) { + return null + } +} +module.exports = truncate diff --git a/test/fixtures/truncations.js b/test/fixtures/truncations.js new file mode 100644 index 00000000..4c28949f --- /dev/null +++ b/test/fixtures/truncations.js @@ -0,0 +1,14 @@ +'use strict' + +// [version, releaseType, result] +// truncate(version, type) -> result +module.exports = [ + ['1.2.3-foo', 'patch', '1.2.3'], + ['1.2.3', 'patch', '1.2.3'], + ['1.2.3', 'minor', '1.2.0'], + ['1.2.3', 'major', '1.0.0'], + + // invalid inputs + ['1.2.3', 'fake', null], + ['fake', 'major', null], +] diff --git a/test/functions/truncate.js b/test/functions/truncate.js new file mode 100644 index 00000000..9c12e6b1 --- /dev/null +++ b/test/functions/truncate.js @@ -0,0 +1,20 @@ +'use strict' + +const { test } = require('tap') +const truncate = require('../../functions/truncate') +const parse = require('../../functions/parse') +const truncations = require('../fixtures/truncations.js') + +test('truncate versions test', (t) => { + truncations.forEach(([pre, what, wanted, options, id, base]) => { + const found = truncate(pre, what, options, id, base) + const cmd = `truncate(${pre}, ${what}, ${options}, ${id}, ${base})` + t.equal(found, wanted, `${cmd} === ${wanted}`) + + const parsed = parse(pre, options) + const semverTruncated = truncate(parsed, what, options) + t.equal(semverTruncated, wanted, `${cmd} works on Semver objects`) + }) + + t.end() +}) From 6463847d7eca040cee61f754401d1b0722765be6 Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Thu, 30 Apr 2026 22:03:01 -0500 Subject: [PATCH 2/9] Use `parse` instead of `new SemVer` in `truncate` --- functions/truncate.js | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/functions/truncate.js b/functions/truncate.js index 563c76f7..27acf879 100644 --- a/functions/truncate.js +++ b/functions/truncate.js @@ -1,36 +1,28 @@ 'use strict' -const SemVer = require('../classes/semver') +const parse = require('./parse') const constants = require('../internal/constants') -const truncate = (version, releaseType, options) => { - // if (typeof (options) === 'string') { - // options = undefined - // } +const _truncate = (version, releaseType) => { + version.prerelease = [] - if (!constants.RELEASE_TYPES.includes(releaseType)) { - return null + switch (releaseType) { + case 'major': + version.minor = 0 + // eslint-disable-next-line no-fallthrough -- this is intentional fallthrough + case 'minor': + version.patch = 0 } - try { - const currentVersion = new SemVer( - version instanceof SemVer ? version.version : version, - options - ) - - currentVersion.prerelease = [] - - switch (releaseType) { - case 'major': - currentVersion.minor = 0 - // eslint-disable-next-line no-fallthrough -- this is intentional fallthrough - case 'minor': - currentVersion.patch = 0 - } + return version.format() +} - return currentVersion.format() - } catch (er) { +const truncate = (version, releaseType, options) => { + if (!constants.RELEASE_TYPES.includes(releaseType)) { return null } + + const parsed = parse(version, options) + return parsed && _truncate(parsed, releaseType) } module.exports = truncate From 0ac2c7129de2d449d55dcdd1becbe658d4148125 Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Sat, 2 May 2026 18:19:22 -0500 Subject: [PATCH 3/9] Remove build info for pre* truncation --- functions/truncate.js | 21 +++++++++++++++------ test/functions/truncate.js | 29 +++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/functions/truncate.js b/functions/truncate.js index 27acf879..23410433 100644 --- a/functions/truncate.js +++ b/functions/truncate.js @@ -3,26 +3,35 @@ const parse = require('./parse') const constants = require('../internal/constants') -const _truncate = (version, releaseType) => { +const _truncate = (version, truncation) => { + if (truncation.startsWith('pre')) { + return version.version + } + version.prerelease = [] - switch (releaseType) { + switch (truncation) { case 'major': version.minor = 0 - // eslint-disable-next-line no-fallthrough -- this is intentional fallthrough + version.patch = 0 + break case 'minor': version.patch = 0 + break } return version.format() } -const truncate = (version, releaseType, options) => { - if (!constants.RELEASE_TYPES.includes(releaseType)) { +const truncate = (version, truncation, options) => { + if (!constants.RELEASE_TYPES.includes(truncation)) { return null } const parsed = parse(version, options) - return parsed && _truncate(parsed, releaseType) + return parsed && _truncate(parsed, truncation) } module.exports = truncate + + +// 1. Should truncate _always_ strip off build info? diff --git a/test/functions/truncate.js b/test/functions/truncate.js index 9c12e6b1..7620135e 100644 --- a/test/functions/truncate.js +++ b/test/functions/truncate.js @@ -4,17 +4,34 @@ const { test } = require('tap') const truncate = require('../../functions/truncate') const parse = require('../../functions/parse') const truncations = require('../fixtures/truncations.js') +const validVersions = require('../fixtures/valid-versions.js') -test('truncate versions test', (t) => { - truncations.forEach(([pre, what, wanted, options, id, base]) => { - const found = truncate(pre, what, options, id, base) - const cmd = `truncate(${pre}, ${what}, ${options}, ${id}, ${base})` - t.equal(found, wanted, `${cmd} === ${wanted}`) +test('truncate fixture versions test', (t) => { + truncations.forEach(([pre, truncation, expected, options, id, base]) => { + const actual = truncate(pre, truncation, options, id, base) + const cmd = `truncate(${pre}, ${truncation}, ${options}, ${id}, ${base})` + t.equal(actual, expected, `${cmd} === ${expected}`) const parsed = parse(pre, options) const semverTruncated = truncate(parsed, what, options) - t.equal(semverTruncated, wanted, `${cmd} works on Semver objects`) + t.equal(semverTruncated, expected, `${cmd} works on Semver objects`) }) t.end() }) + +test('truncate pre* only removes build info', (t) => { + ['prerelease', 'prepatch', 'preminor', 'premajor'].forEach(what => { + validVersions.forEach((v) => { + const versionToTruncate = v[0] + const parsed = parse(versionToTruncate) + const expected = parsed.version + const actual = truncate(versionToTruncate, what) + + const cmd = `truncate(${versionToTruncate}, ${what})` + t.equal(actual, expected, `${cmd} === ${expected}`) + t.same(parse(actual).build, [], `${cmd} strips build info `) + }) + }) + t.end() +}) \ No newline at end of file From 7058c780950183333b63573f6f21bbca3643b915 Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Sat, 2 May 2026 18:33:29 -0500 Subject: [PATCH 4/9] Improve variable and function names --- functions/truncate.js | 26 ++++++++++++++------------ test/functions/truncate.js | 4 ++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/functions/truncate.js b/functions/truncate.js index 23410433..ee9dabf9 100644 --- a/functions/truncate.js +++ b/functions/truncate.js @@ -3,8 +3,17 @@ const parse = require('./parse') const constants = require('../internal/constants') -const _truncate = (version, truncation) => { - if (truncation.startsWith('pre')) { +const truncate = (version, truncation, options) => { + if (!constants.RELEASE_TYPES.includes(truncation)) { + return null + } + + const parsed = parse(version, options) + return parsed && doTruncation(parsed, truncation) +} + +const doTruncation = (version, truncation) => { + if (isPrerelease(truncation)) { return version.version } @@ -23,15 +32,8 @@ const _truncate = (version, truncation) => { return version.format() } -const truncate = (version, truncation, options) => { - if (!constants.RELEASE_TYPES.includes(truncation)) { - return null - } - - const parsed = parse(version, options) - return parsed && _truncate(parsed, truncation) +const isPrerelease = (type) => { + return type.startsWith('pre') } -module.exports = truncate - -// 1. Should truncate _always_ strip off build info? +module.exports = truncate diff --git a/test/functions/truncate.js b/test/functions/truncate.js index 7620135e..40558d5c 100644 --- a/test/functions/truncate.js +++ b/test/functions/truncate.js @@ -13,7 +13,7 @@ test('truncate fixture versions test', (t) => { t.equal(actual, expected, `${cmd} === ${expected}`) const parsed = parse(pre, options) - const semverTruncated = truncate(parsed, what, options) + const semverTruncated = truncate(parsed, truncation, options) t.equal(semverTruncated, expected, `${cmd} works on Semver objects`) }) @@ -34,4 +34,4 @@ test('truncate pre* only removes build info', (t) => { }) }) t.end() -}) \ No newline at end of file +}) From 4cc9cd53d3a175a3092d0f271e50a15911a790c4 Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Sat, 2 May 2026 18:52:57 -0500 Subject: [PATCH 5/9] Eliminate unused vars in truncate tests --- test/functions/truncate.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/functions/truncate.js b/test/functions/truncate.js index 40558d5c..46788aa4 100644 --- a/test/functions/truncate.js +++ b/test/functions/truncate.js @@ -7,13 +7,13 @@ const truncations = require('../fixtures/truncations.js') const validVersions = require('../fixtures/valid-versions.js') test('truncate fixture versions test', (t) => { - truncations.forEach(([pre, truncation, expected, options, id, base]) => { - const actual = truncate(pre, truncation, options, id, base) - const cmd = `truncate(${pre}, ${truncation}, ${options}, ${id}, ${base})` + truncations.forEach(([pre, truncation, expected]) => { + const actual = truncate(pre, truncation) + const cmd = `truncate(${pre}, ${truncation})` t.equal(actual, expected, `${cmd} === ${expected}`) - const parsed = parse(pre, options) - const semverTruncated = truncate(parsed, truncation, options) + const parsed = parse(pre) + const semverTruncated = truncate(parsed, truncation) t.equal(semverTruncated, expected, `${cmd} works on Semver objects`) }) @@ -30,7 +30,7 @@ test('truncate pre* only removes build info', (t) => { const cmd = `truncate(${versionToTruncate}, ${what})` t.equal(actual, expected, `${cmd} === ${expected}`) - t.same(parse(actual).build, [], `${cmd} strips build info `) + t.same(parse(actual).build, [], `${cmd} build info should be removed`) }) }) t.end() From f76d67d2cf8119d4183d9c6c2caec923cacbfb6a Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Sat, 2 May 2026 19:12:26 -0500 Subject: [PATCH 6/9] Add truncate function to index, README --- README.md | 8 ++++++++ index.js | 2 ++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index a7f243c2..c71f7607 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ const semverCompareLoose = require('semver/functions/compare-loose') const semverCompareBuild = require('semver/functions/compare-build') const semverSort = require('semver/functions/sort') const semverRsort = require('semver/functions/rsort') +const semverTruncate = require('semver/functions/truncate') // low-level comparators between versions const semverGt = require('semver/functions/gt') @@ -456,6 +457,12 @@ strings that they parse. or comparators intersect. * `parse(v)`: Attempt to parse a string as a semantic version, returning either a `SemVer` object or `null`. +* `truncate(v, releaseType)`: Return the version with components _lower_ + than `releaseType` dropped off, e.g.: + * `major` removes build & prerelease info and sets minor & patch to 0. + * `minor` removes build & prerelease info, and sets patch to 0 + * `patch` removes build & prerelease info + * All prerelease types remove build info only ### Comparison @@ -657,6 +664,7 @@ The following modules are available: * `require('semver/functions/rsort')` * `require('semver/functions/satisfies')` * `require('semver/functions/sort')` +* `require('semver/functions.truncate')` * `require('semver/functions/valid')` * `require('semver/ranges/gtr')` * `require('semver/ranges/intersects')` diff --git a/index.js b/index.js index 285662ac..bc1f608c 100644 --- a/index.js +++ b/index.js @@ -28,6 +28,7 @@ const gte = require('./functions/gte') const lte = require('./functions/lte') const cmp = require('./functions/cmp') const coerce = require('./functions/coerce') +const truncate = require('./functions/truncate') const Comparator = require('./classes/comparator') const Range = require('./classes/range') const satisfies = require('./functions/satisfies') @@ -66,6 +67,7 @@ module.exports = { lte, cmp, coerce, + truncate, Comparator, Range, satisfies, From 40d5827061c8c95ae828ccdfeb6bf7ef26f2ee52 Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Mon, 4 May 2026 21:50:30 -0500 Subject: [PATCH 7/9] Apply suggestion from @owlstronaut Co-authored-by: Michael Smith --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c71f7607..5e03abcc 100644 --- a/README.md +++ b/README.md @@ -664,7 +664,7 @@ The following modules are available: * `require('semver/functions/rsort')` * `require('semver/functions/satisfies')` * `require('semver/functions/sort')` -* `require('semver/functions.truncate')` +* `require('semver/functions/truncate')` * `require('semver/functions/valid')` * `require('semver/ranges/gtr')` * `require('semver/ranges/intersects')` From 924e60b94b81b359b348b72e14c13c5a7a627a24 Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Wed, 6 May 2026 20:09:45 -0500 Subject: [PATCH 8/9] Ensure SemVer object inputs are not mutated --- functions/truncate.js | 13 +++++++++++-- test/functions/truncate.js | 14 +++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/functions/truncate.js b/functions/truncate.js index ee9dabf9..8314e4e9 100644 --- a/functions/truncate.js +++ b/functions/truncate.js @@ -2,14 +2,23 @@ const parse = require('./parse') const constants = require('../internal/constants') +const SemVer = require('../classes/semver') const truncate = (version, truncation, options) => { if (!constants.RELEASE_TYPES.includes(truncation)) { return null } - const parsed = parse(version, options) - return parsed && doTruncation(parsed, truncation) + const clonedVersion = cloneInputVersion(version, options) + return clonedVersion && doTruncation(clonedVersion, truncation) +} + +const cloneInputVersion = (version, options) => { + const versionStringToParse = ( + version instanceof SemVer ? version.version : version + ) + + return parse(versionStringToParse, options) } const doTruncation = (version, truncation) => { diff --git a/test/functions/truncate.js b/test/functions/truncate.js index 46788aa4..c1f7aa1b 100644 --- a/test/functions/truncate.js +++ b/test/functions/truncate.js @@ -6,15 +6,23 @@ const parse = require('../../functions/parse') const truncations = require('../fixtures/truncations.js') const validVersions = require('../fixtures/valid-versions.js') +// Freezing SemVer object inputs to truncate ensures that the truncate function +// does not mutate them +const parseAndFreezeSemVerObject = (version) => { + const parsed = parse(version) + Object.freeze(parsed) + return parsed +} + test('truncate fixture versions test', (t) => { truncations.forEach(([pre, truncation, expected]) => { const actual = truncate(pre, truncation) const cmd = `truncate(${pre}, ${truncation})` t.equal(actual, expected, `${cmd} === ${expected}`) - const parsed = parse(pre) + const parsed = parseAndFreezeSemVerObject(pre) const semverTruncated = truncate(parsed, truncation) - t.equal(semverTruncated, expected, `${cmd} works on Semver objects`) + t.equal(semverTruncated, expected, `${cmd} works on Semver object inputs`) }) t.end() @@ -24,7 +32,7 @@ test('truncate pre* only removes build info', (t) => { ['prerelease', 'prepatch', 'preminor', 'premajor'].forEach(what => { validVersions.forEach((v) => { const versionToTruncate = v[0] - const parsed = parse(versionToTruncate) + const parsed = parseAndFreezeSemVerObject(versionToTruncate) const expected = parsed.version const actual = truncate(versionToTruncate, what) From d8bad09d3b30193b074d4e46ead76fdfa3890e49 Mon Sep 17 00:00:00 2001 From: Patrick Johnmeyer Date: Wed, 6 May 2026 20:24:13 -0500 Subject: [PATCH 9/9] Add more prerelease+build test fixtures for truncate --- test/fixtures/truncations.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/fixtures/truncations.js b/test/fixtures/truncations.js index 4c28949f..a4ef84cc 100644 --- a/test/fixtures/truncations.js +++ b/test/fixtures/truncations.js @@ -11,4 +11,29 @@ module.exports = [ // invalid inputs ['1.2.3', 'fake', null], ['fake', 'major', null], + + // additional pre-release, build, and pre+build inputs + ['4.5.6-rc2', 'prerelease', '4.5.6-rc2'], + ['4.5.6-rc2', 'prepatch', '4.5.6-rc2'], + ['4.5.6-rc2', 'preminor', '4.5.6-rc2'], + ['4.5.6-rc2', 'premajor', '4.5.6-rc2'], + ['4.5.6-rc2', 'patch', '4.5.6'], + ['4.5.6-rc2', 'minor', '4.5.0'], + ['4.5.6-rc2', 'major', '4.0.0'], + + ['4.5.6+dadb0d', 'prerelease', '4.5.6'], + ['4.5.6+dadb0d', 'prepatch', '4.5.6'], + ['4.5.6+dadb0d', 'preminor', '4.5.6'], + ['4.5.6+dadb0d', 'premajor', '4.5.6'], + ['4.5.6+dadb0d', 'patch', '4.5.6'], + ['4.5.6+dadb0d', 'minor', '4.5.0'], + ['4.5.6+dadb0d', 'major', '4.0.0'], + + ['4.5.6-rc2+dadb0d', 'prerelease', '4.5.6-rc2'], + ['4.5.6-rc2+dadb0d', 'prepatch', '4.5.6-rc2'], + ['4.5.6-rc2+dadb0d', 'preminor', '4.5.6-rc2'], + ['4.5.6-rc2+dadb0d', 'premajor', '4.5.6-rc2'], + ['4.5.6-rc2+dadb0d', 'patch', '4.5.6'], + ['4.5.6-rc2+dadb0d', 'minor', '4.5.0'], + ['4.5.6-rc2+dadb0d', 'major', '4.0.0'], ]