Skip to content

feat: allow strict semver check #789

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ semver.lt('1.2.3', '9.8.7') // true
semver.minVersion('>=1.0.0') // '1.0.0'
semver.valid(semver.coerce('v2')) // '2.0.0'
semver.valid(semver.coerce('42.6.7.9.3-alpha')) // '42.6.7'

// Strict mode - rejects version strings with 'v' prefix
semver.valid('v1.2.3') // '1.2.3' (default behavior)
semver.valid('v1.2.3', { strict: true }) // throws TypeError
semver.valid('1.2.3', { strict: true }) // '1.2.3'
```

You can also just load the module for the function that you care about if
Expand Down Expand Up @@ -116,6 +121,10 @@ Options:
-p --include-prerelease
Always include prerelease versions in range matching

-s --strict
Reject version strings with leading 'v' prefix
(throws error if version starts with 'v' or 'V')

-c --coerce
Coerce a string into SemVer if possible
(does not imply --loose)
Expand Down Expand Up @@ -419,6 +428,10 @@ are:
behavior](https://github.com/npm/node-semver#prerelease-tags) of
excluding prerelease tagged versions from ranges unless they are
explicitly opted into.
- `strict`: When set to `true`, versions with leading "v" or "V" prefixes
will be rejected and throw a TypeError. By default (when `false`), such
prefixes are allowed and stripped. This option enforces strict SemVer
compliance where version strings must not contain prefixes.

Strict-mode Comparators and Ranges will be strict about the SemVer
strings that they parse.
Expand Down
24 changes: 22 additions & 2 deletions bin/semver.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ let coerce = false

let rtl = false

let strict = false

let identifier

let identifierBase
Expand Down Expand Up @@ -56,6 +58,9 @@ const main = () => {
case '-p': case '--include-prerelease':
includePrerelease = true
break
case '-s': case '--strict':
strict = true
break
case '-v': case '--version':
versions.push(argv.shift())
break
Expand Down Expand Up @@ -100,12 +105,23 @@ const main = () => {
}
}

options = parseOptions({ loose, includePrerelease, rtl })
options = parseOptions({ loose, includePrerelease, rtl, strict })

versions = versions.map((v) => {
return coerce ? (semver.coerce(v, options) || { version: v }).version : v
}).filter((v) => {
return semver.valid(v)
if (strict) {
// In strict mode, throw errors instead of filtering
try {
new (require('../classes/semver'))(v, options)
return true
} catch (err) {
console.error(`Error: ${err.message}`)
process.exit(1)
}
} else {
return semver.valid(v)
}
})
if (!versions.length) {
return fail()
Expand Down Expand Up @@ -165,6 +181,10 @@ Options:
-p --include-prerelease
Always include prerelease versions in range matching

-s --strict
Reject version strings with leading 'v' prefix
(throws error if version starts with 'v' or 'V')

-c --coerce
Coerce a string into SemVer if possible
(does not imply --loose)
Expand Down
5 changes: 5 additions & 0 deletions classes/semver.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class SemVer {
// don't run into trouble passing this.options around.
this.includePrerelease = !!options.includePrerelease

// Check for strict mode - reject versions with leading 'v'
if (options.strict && /^v/i.test(version.trim())) {
throw new TypeError(`Invalid version in strict mode: version cannot start with 'v'. Got: ${version}`)
}

const m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL])

if (!m) {
Expand Down
79 changes: 70 additions & 9 deletions tap-snapshots/test/bin/semver.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ Object {
-p --include-prerelease
Always include prerelease versions in range matching

-s --strict
Reject version strings with leading 'v' prefix
(throws error if version starts with 'v' or 'V')

-c --coerce
Coerce a string into SemVer if possible
(does not imply --loose)
Expand Down Expand Up @@ -144,6 +148,10 @@ Object {
-p --include-prerelease
Always include prerelease versions in range matching

-s --strict
Reject version strings with leading 'v' prefix
(throws error if version starts with 'v' or 'V')

-c --coerce
Coerce a string into SemVer if possible
(does not imply --loose)
Expand Down Expand Up @@ -205,6 +213,10 @@ Object {
-p --include-prerelease
Always include prerelease versions in range matching

-s --strict
Reject version strings with leading 'v' prefix
(throws error if version starts with 'v' or 'V')

-c --coerce
Coerce a string into SemVer if possible
(does not imply --loose)
Expand Down Expand Up @@ -266,6 +278,10 @@ Object {
-p --include-prerelease
Always include prerelease versions in range matching

-s --strict
Reject version strings with leading 'v' prefix
(throws error if version starts with 'v' or 'V')

-c --coerce
Coerce a string into SemVer if possible
(does not imply --loose)
Expand Down Expand Up @@ -348,15 +364,6 @@ Object {
}
`

exports[`test/bin/semver.js TAP inc tests > -i release 1.0.0-pre`] = `
Object {
"code": 0,
"err": "",
"out": "1.0.0\\n",
"signal": null,
}
`

exports[`test/bin/semver.js TAP sorting and filtering > 1.2.3 -v 3.2.1 --version 2.3.4 -rv 1`] = `
Object {
"code": 0,
Expand Down Expand Up @@ -484,3 +491,57 @@ Object {
"signal": null,
}
`

exports[`test/bin/semver.js TAP strict mode tests > 1.2.3 --strict 1`] = `
Object {
"code": 0,
"err": "",
"out": "1.2.3\\n",
"signal": null,
}
`

exports[`test/bin/semver.js TAP strict mode tests > 1.2.3-alpha.1 -s 1`] = `
Object {
"code": 0,
"err": "",
"out": "1.2.3-alpha.1\\n",
"signal": null,
}
`

exports[`test/bin/semver.js TAP strict mode tests > V1.2.3 -s 1`] = `
Object {
"code": 1,
"err": "Error: Invalid version in strict mode: version cannot start with 'v'. Got: V1.2.3\\n",
"out": "",
"signal": null,
}
`

exports[`test/bin/semver.js TAP strict mode tests > V1.2.3 1`] = `
Object {
"code": 1,
"err": "",
"out": "",
"signal": null,
}
`

exports[`test/bin/semver.js TAP strict mode tests > v1.2.3 --strict 1`] = `
Object {
"code": 1,
"err": "Error: Invalid version in strict mode: version cannot start with 'v'. Got: v1.2.3\\n",
"out": "",
"signal": null,
}
`

exports[`test/bin/semver.js TAP strict mode tests > v1.2.3 1`] = `
Object {
"code": 0,
"err": "",
"out": "1.2.3\\n",
"signal": null,
}
`
12 changes: 12 additions & 0 deletions test/bin/semver.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,15 @@ t.test('args with equals', t => Promise.all([
t.equal(res1.out.trim(), expected)
t.strictSame(res1, res2, args.join(' '))
})))

t.test('strict mode tests', t => Promise.all([
// Default behavior - v-prefix allowed
['v1.2.3'],
['V1.2.3'],
// Strict mode rejects v-prefix
['v1.2.3', '--strict'],
['V1.2.3', '-s'],
// Strict mode allows normal versions
['1.2.3', '--strict'],
['1.2.3-alpha.1', '-s'],
].map(args => t.resolveMatchSnapshot(run(args), args.join(' ')))))
93 changes: 93 additions & 0 deletions test/classes/semver.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,96 @@ test('compareBuild', (t) => {

t.end()
})

test('strict option behavior', (t) => {
// Default behavior - v-prefix allowed
t.test('default allows v-prefix', (t) => {
const v = new SemVer('v1.2.3')
t.equal(v.version, '1.2.3')
t.equal(v.major, 1)
t.equal(v.minor, 2)
t.equal(v.patch, 3)
t.end()
})

// Explicit strict: false - v-prefix allowed
t.test('strict: false allows v-prefix', (t) => {
const v = new SemVer('v1.2.3', { strict: false })
t.equal(v.version, '1.2.3')
t.equal(v.major, 1)
t.equal(v.minor, 2)
t.equal(v.patch, 3)
t.end()
})

// Strict: true without v-prefix - should work
t.test('strict: true allows valid version', (t) => {
const v = new SemVer('1.2.3', { strict: true })
t.equal(v.version, '1.2.3')
t.equal(v.major, 1)
t.equal(v.minor, 2)
t.equal(v.patch, 3)
t.end()
})

// Strict: true with prerelease - should work
t.test('strict: true allows valid prerelease version', (t) => {
const v = new SemVer('1.2.3-alpha.1', { strict: true })
t.equal(v.version, '1.2.3-alpha.1')
t.equal(v.major, 1)
t.equal(v.minor, 2)
t.equal(v.patch, 3)
t.strictSame(v.prerelease, ['alpha', 1])
t.end()
})

// Strict: true with lowercase v-prefix - should throw
t.test('strict: true rejects lowercase v-prefix', (t) => {
t.throws(
() => new SemVer('v1.2.3', { strict: true }),
{
name: 'TypeError',
message: "Invalid version in strict mode: version cannot start with 'v'. Got: v1.2.3",
}
)
t.end()
})

// Strict: true with uppercase V-prefix - should throw
t.test('strict: true rejects uppercase V-prefix', (t) => {
t.throws(
() => new SemVer('V1.2.3', { strict: true }),
{
name: 'TypeError',
message: "Invalid version in strict mode: version cannot start with 'v'. Got: V1.2.3",
}
)
t.end()
})

// Strict: true with v-prefix and prerelease - should throw
t.test('strict: true rejects v-prefix with prerelease', (t) => {
t.throws(
() => new SemVer('v1.2.3-alpha.1', { strict: true }),
{
name: 'TypeError',
message: "Invalid version in strict mode: version cannot start with 'v'. Got: v1.2.3-alpha.1",
}
)
t.end()
})

// Strict: true with whitespace and v-prefix - should throw
t.test('strict: true rejects v-prefix with whitespace', (t) => {
t.throws(
() => new SemVer(' v1.2.3 ', { strict: true }),
{
name: 'TypeError',
message: "Invalid version in strict mode: version cannot start with 'v'. Got: v1.2.3 ",
}
)
t.end()
})

t.end()
})
7 changes: 7 additions & 0 deletions test/internal/parse-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ t.test('any object passed is returned', t => {
t.strictSame(parseOptions({ loose: true }), { loose: true })
t.strictSame(parseOptions({ rtl: true }), { rtl: true })
t.strictSame(parseOptions({ includePrerelease: true }), { includePrerelease: true })
t.strictSame(parseOptions({ strict: true }), { strict: true })
t.strictSame(parseOptions({ strict: false }), { strict: false })
t.strictSame(parseOptions({ loose: true, rtl: true }), { loose: true, rtl: true })
t.strictSame(parseOptions({ loose: true, includePrerelease: true }), {
loose: true,
Expand All @@ -41,5 +43,10 @@ t.test('any object passed is returned', t => {
rtl: true,
includePrerelease: true,
})
t.strictSame(parseOptions({ loose: true, includePrerelease: true, strict: true }), {
loose: true,
includePrerelease: true,
strict: true,
})
t.end()
})