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: preserve pre-release and build parts of a version on coerce (#592) #671

Merged
merged 12 commits into from
Jan 31, 2024
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,10 @@ tuple. For example, `1.2.3.4` will return `2.3.4` in rtl mode, not
`4.0.0`. `1.2.3/4` will return `4.0.0`, because the `4` is not a part of
any other overlapping SemVer tuple.

If the `options.includePrerelease` flag is set, then the `coerce` result will contain
prerelease and build parts of a version. For example, `1.2.3.4-rc.1+rev.2`
will preserve prerelease `rc.1` and build `rev.2` in the result.

### Clean

* `clean(version)`: Clean a string to be a valid semver if possible
Expand Down
18 changes: 13 additions & 5 deletions functions/coerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,42 @@ const coerce = (version, options) => {

let match = null
if (!options.rtl) {
match = version.match(re[t.COERCE])
match = version.match(options.includePrerelease ? re[t.COERCEFULL] : re[t.COERCE])
} else {
// Find the right-most coercible string that does not share
// a terminus with a more left-ward coercible string.
// Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4'
// With includePrerelease option set, '1.2.3.4-rc' wants to coerce '2.3.4-rc', not '2.3.4'
//
// Walk through the string checking with a /g regexp
// Manually set the index so as to pick up overlapping matches.
// Stop when we get a match that ends at the string end, since no
// coercible string can be more right-ward without the same terminus.
const coerceRtlRegex = options.includePrerelease ? re[t.COERCERTLFULL] : re[t.COERCERTL]
let next
while ((next = re[t.COERCERTL].exec(version)) &&
while ((next = coerceRtlRegex.exec(version)) &&
(!match || match.index + match[0].length !== version.length)
) {
if (!match ||
next.index + next[0].length !== match.index + match[0].length) {
match = next
}
re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length
coerceRtlRegex.lastIndex = next.index + next[1].length + next[2].length
}
// leave it in a clean state
re[t.COERCERTL].lastIndex = -1
coerceRtlRegex.lastIndex = -1
}

if (match === null) {
return null
}

return parse(`${match[2]}.${match[3] || '0'}.${match[4] || '0'}`, options)
const major = match[2]
const minor = match[3] || '0'
const patch = match[4] || '0'
const prerelease = options.includePrerelease && match[5] ? `-${match[5]}` : ''
const build = options.includePrerelease && match[6] ? `+${match[6]}` : ''

return parse(`${major}.${minor}.${patch}${prerelease}${build}`, options)
}
module.exports = coerce
9 changes: 7 additions & 2 deletions internal/re.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,17 @@ createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`)

// Coercion.
// Extract anything that could conceivably be a part of a valid semver
createToken('COERCE', `${'(^|[^\\d])' +
createToken('COERCEPLAIN', `${'(^|[^\\d])' +
'(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` +
`(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` +
`(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` +
`(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?`)
createToken('COERCE', `${t.COERCEPLAIN}(?:$|[^\\d])`)
createToken('COERCEFULL', `${COERCEPLAIN}` +
`(?:${src[t.PRERELEASE]})?` +
`(?:${src[t.BUILD]})?` +
`(?:$|[^\\d])`)
createToken('COERCERTL', src[t.COERCE], true)
createToken('COERCERTLFULL', src[t.COERCEFULL], true)

// Tilde ranges.
// Meaning is "reasonably at or greater than"
Expand Down
51 changes: 48 additions & 3 deletions test/functions/coerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,58 @@ test('coerce tests', (t) => {
['1.2.3/6', '6.0.0', { rtl: true }],
['1.2.3.4', '2.3.4', { rtl: true }],
['1.2.3.4xyz', '2.3.4', { rtl: true }],

['1-rc.5', '1.0.0-rc.5', { includePrerelease: true }, true],
['1.2-rc.5', '1.2.0-rc.5', { includePrerelease: true }, true],
['1.2.3-rc.5', '1.2.3-rc.5', { includePrerelease: true }, true],
['1.2.3-rc.5/a', '1.2.3-rc.5', { includePrerelease: true }, true],
['1.2.3.4-rc.5', '1.2.3', { includePrerelease: true }, true],
['1.2.3.4+rev.6', '1.2.3', { includePrerelease: true }, true],

['1+rev.6', '1.0.0+rev.6', { includePrerelease: true }, true],
['1.2+rev.6', '1.2.0+rev.6', { includePrerelease: true }, true],
['1.2.3+rev.6', '1.2.3+rev.6', { includePrerelease: true }, true],
['1.2.3+rev.6/a', '1.2.3+rev.6', { includePrerelease: true }, true],
['1.2.3.4-rc.5', '1.2.3', { includePrerelease: true }, true],
['1.2.3.4+rev.6', '1.2.3', { includePrerelease: true }, true],

['1-rc.5+rev.6', '1.0.0-rc.5+rev.6', { includePrerelease: true }, true],
['1.2-rc.5+rev.6', '1.2.0-rc.5+rev.6', { includePrerelease: true }, true],
['1.2.3-rc.5+rev.6', '1.2.3-rc.5+rev.6', { includePrerelease: true }, true],
['1.2.3-rc.5+rev.6/a', '1.2.3-rc.5+rev.6', { includePrerelease: true }, true],

['1.2-rc.5+rev.6', '1.2.0-rc.5+rev.6', { rtl: true, includePrerelease: true }, true],
['1.2.3-rc.5+rev.6', '1.2.3-rc.5+rev.6', { rtl: true, includePrerelease: true }, true],
['1.2.3.4-rc.5+rev.6', '2.3.4-rc.5+rev.6', { rtl: true, includePrerelease: true }, true],
['1.2.3.4-rc.5', '2.3.4-rc.5', { rtl: true, includePrerelease: true }, true],
['1.2.3.4+rev.6', '2.3.4+rev.6', { rtl: true, includePrerelease: true }, true],
['1.2.3.4-rc.5+rev.6/7', '7.0.0', { rtl: true, includePrerelease: true }, true],
['1.2.3.4-rc/7.5+rev.6', '7.5.0+rev.6', { rtl: true, includePrerelease: true }, true],
['1.2.3.4/7-rc.5+rev.6', '7.0.0-rc.5+rev.6', { rtl: true, includePrerelease: true }, true],
]
coerceToValid.forEach(([input, expected, options]) => {
const msg = `coerce(${input}) should become ${expected}`
t.same((coerce(input, options) || {}).version, expected, msg)
coerceToValid.forEach(([input, expected, options, shouldParse]) => {
const coerceExpression = `coerce(${input}, ${JSON.stringify(options)})`
const coercedVersion = coerce(input, options) || {}
let expectedText = expected

if (shouldParse) {
wraithgar marked this conversation as resolved.
Show resolved Hide resolved
const expectedVersion = parse(expected)
expectedText = expectedVersion.version
t.equal(
expectedVersion.compare(coercedVersion),
0,
`${coerceExpression} should be equal to ${expectedVersion}`
)
}
t.same(
coercedVersion.version,
expectedText,
`${coerceExpression} should become ${expectedText}`
)
wraithgar marked this conversation as resolved.
Show resolved Hide resolved
})

t.same(valid(coerce('42.6.7.9.3-alpha')), '42.6.7')
t.same(valid(coerce('42.6.7-alpha+rev.1', { includePrerelease: true })), '42.6.7-alpha')
t.same(valid(coerce('v2')), '2.0.0')

t.end()
Expand Down