node-semver icon indicating copy to clipboard operation
node-semver copied to clipboard

Why does `>1.2` throw as an invalid comparator?

Open jsumners opened this issue 4 months ago • 18 comments

https://github.com/npm/node-semver/blob/d17aebf8485edfe9dda982dab578c603d031e4ab/test/fixtures/range-exclude.js#L68

I am trying to understand why npx -r '>1.2' 1.2.8 results in 1.2.8 not satisfying the range. I think >1.2 should expand to >1.2.0 <1.3.0 which would mean 1.2.8 satisfies. But stepping through the tool, I see it hits this path:

https://github.com/npm/node-semver/blob/d17aebf8485edfe9dda982dab578c603d031e4ab/classes/comparator.js#L37-L42

The result being m === null and the TypeError is thrown.

jsumners avatar Aug 27 '25 22:08 jsumners

>1.2 does not expand to >1.2.0, it equates to >= 1.3. In other words, > 1.2 means that no 1.2.x version can satisfy it.

ljharb avatar Aug 27 '25 22:08 ljharb

I see. Thank you.

jsumners avatar Aug 28 '25 00:08 jsumners

Sorry, I'm reopening this. I think this line is confirming your answer @ljharb:

https://github.com/npm/node-semver/blob/d17aebf8485edfe9dda982dab578c603d031e4ab/README.md?plain=1#L164-L166

But further down we have:

https://github.com/npm/node-semver/blob/d17aebf8485edfe9dda982dab578c603d031e4ab/README.md?plain=1#L316-L321

This seems to be a conflict to me.

jsumners avatar Aug 28 '25 10:08 jsumners

1.2 is not the same as >1.2

wraithgar avatar Aug 28 '25 16:08 wraithgar

1.2 is a component of >1.2. The version string should still expand the same.

jsumners avatar Aug 28 '25 16:08 jsumners

1.2 does not expand to 1.2.0. It expands to 1.2.x, and >1.2.x indeed excludes any 1.2.

ljharb avatar Aug 28 '25 16:08 ljharb

It expands to 1.2.x

Which expands to 1.2.0. Thus, >1.2 should expand to >1.2.0, right?

jsumners avatar Aug 28 '25 16:08 jsumners

No, it does not expand to 1.2.0. It can't expand without a concrete list of existing versions, and it will always match the latest 1.2, not the .0

ljharb avatar Aug 28 '25 17:08 ljharb

I'm just not seeing the rule you're basing that on. If the readme isn't the source of truth for the expansion rules, please point me to the right document.

jsumners avatar Aug 28 '25 18:08 jsumners

It’s not documented that i can see, but it’s common sense - would “1.2” be satisfied by 1.2.99? Obviously yes, because it’s a range, not a short way to express one version.

ljharb avatar Aug 28 '25 20:08 ljharb

Sorry, you've just confused me even more. I originally asserted that 1.2.8 should satisfy >1.2. Your response was that it shouldn't. Now you're suggesting it should.

jsumners avatar Aug 29 '25 10:08 jsumners

1.2.8 certainly satisfies 1.2. It certainly does not satisfy >1.2 (which is the same as >= 1.3).

ljharb avatar Aug 30 '25 04:08 ljharb

This discussion will continue for years until there's an npm specification :)

@jsumners , As you, I'm surprised me because I thought semver.coerce() was being applied before the comparison. Then I reasoned that it was checking for inclusion in a set, so the logical thing to do was to convert '1.2' to '1.2.x' and not coerce to '1.2.0' (the same for 1 to 1.x.x). After that, the results in this list made perfect sense to me...

semver.satisfies('1.2.0', '1')        // true -- '1' is converted to '1.x.x'
semver.satisfies('1.2.0', '1.2')      // true -- '1.2' to '1.2.x' an so on
semver.satisfies('1.2.0', '1.2.0')    // true
semver.satisfies('1.2.0', '=1.2')     // true -- The '=' makes no difference
semver.satisfies('1.2.0', '=1.2.0')   // true
semver.satisfies('1.2.0', '~1.2')     // true -- '~#.#' is equal to '#.#.x'
semver.satisfies('1.2.0', '~1.2.0')   // true
semver.satisfies('1.2.0', '<1.2')     // false
semver.satisfies('1.2.0', '<1.2.0')   // false
semver.satisfies('1.2.0', '<=1.2')    // true
semver.satisfies('1.2.0', '<=1.2.0')  // true
semver.satisfies('1.2.0', '>1.2')     // false
semver.satisfies('1.2.0', '>1.2.0')   // false
semver.satisfies('1.2.0', '>=1.2')    // true
semver.satisfies('1.2.0', '>=1.2.0')  // true
semver.satisfies('1.2.0', '^1.2')     // true
semver.satisfies('1.2.0', '^1.2.0')   // true -- '^#.#' is equal to '#.x.x'

...as this...

semver.satisfies('1.2.8', '1')        // true
semver.satisfies('1.2.8', '1.2')      // true
semver.satisfies('1.2.8', '1.2.0')    // false
semver.satisfies('1.2.8', '=1.2')     // true
semver.satisfies('1.2.8', '=1.2.0')   // false
semver.satisfies('1.2.8', '~1.2')     // true
semver.satisfies('1.2.8', '~1.2.0')   // true
semver.satisfies('1.2.8', '<1.2')     // false
semver.satisfies('1.2.8', '<1.2.0')   // false
semver.satisfies('1.2.8', '<=1.2')    // true
semver.satisfies('1.2.8', '<=1.2.0')  // false
semver.satisfies('1.2.8', '>1.2')     // false
semver.satisfies('1.2.8', '>1.2.0')   // true
semver.satisfies('1.2.8', '>=1.2')    // true
semver.satisfies('1.2.8', '>=1.2.0')  // true
semver.satisfies('1.2.8', '^1.2')     // true
semver.satisfies('1.2.8', '^1.2.0')   // true

satisfies does not work as other semver methods ...gt, lt, etc. The conclusión is to use the full semver (n.n.n) if you're unsure.

This is the gist with the source

aMarCruz avatar Sep 01 '25 20:09 aMarCruz

@ljharb does this list of rules look correct to you?

  1. A version string with all three primary positions provided specifies an exact version, e.g. 1.2.3 is equivalent to =1.2.3.
  2. A version string with only one or two primary positions provided equates to an X-range, e.g 1.2 is equivalent to 1.2.x is equivalent to >=1.2.0 <1.3.0 and 1 is equivalent to 1.x.x is equivalent to >=1.0.0 <2.0.0.
  3. A version string prefixed with any primitive (< | > | >= | <= | =) is not an X-range and only the positions provided count, e.g >1.2.3 is equivalent to >=1.2.4, >1.2 is equivalent to >=1.3.0, and >1 is equivalent to >=2.0.0.
  4. Tilde and Caret range expansions are described fully in the readme.

jsumners avatar Sep 01 '25 20:09 jsumners

There's some nuance you're missing about prereleases, but otherwise that sounds correct.

ljharb avatar Sep 01 '25 22:09 ljharb

  1. A version string with all three primary positions provided specifies an exact version, e.g. 1.2.3 is equivalent to =1.2.3.

yes

  1. A version string with only one or two primary positions provided equates to an X-range, e.g 1.2 is equivalent to 1.2.x is equivalent to >=1.2.0 <1.3.0 and 1 is equivalent to 1.x.x is equivalent to >=1.0.0 <2.0.0.

right

  1. A version string prefixed with any primitive (< | > | >= | <= | =) is not an X-range and only the positions provided count, e.g >1.2.3 is equivalent to >=1.2.4, >1.2 is equivalent to >=1.3.0, and >1 is equivalent to >=2.0.0.

In the context of semver.satisfy() all the shortened versions are X-ranges. That's why it's better to use the full version with comparison operators, as in this example from eslint's package.json.

{
  "engines": {
    "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
  }
}

See the output of semver.toComparators('^18.18.0 || ^20.9.0 || >=21.1.0') ...

[
  [ '>=18.18.0', '<19.0.0-0' ],
  [ '>=20.9.0', '<21.0.0-0' ],
  [ '>=21.1.0' ]
]

Saludos desde México.

aMarCruz avatar Sep 02 '25 04:09 aMarCruz

Indeed, ^18.8 and ^18.8.0 are identical. (typically the shorter one is preferred)

ljharb avatar Sep 02 '25 04:09 ljharb

@aMarCruz thank you for the information. I am not concerned with the various methods exported by this module. I am solely concerned with how strings are meant to be parsed and interpreted. In that regard, your example for semver.satisfy is covered by the fourth "rule," not the third one.

jsumners avatar Sep 02 '25 10:09 jsumners