[GH-ISSUE #948] Support multiple version axes #4834

Open
opened 2026-06-13 13:14:46 -05:00 by GiteaMirror · 9 comments
Owner

Originally created by @steinybot on GitHub (May 25, 2023).
Original GitHub issue: https://github.com/semver/semver/issues/948

There are situations where an API has multiple axes for compatibility.

Typical examples include:

  • Vendored/bundled libraries
  • Transpiling from one language/tool to another
  • Cross building for different target environments

Consider a JavaScript library with TypeScript definitions where those TypeScript definitions are published separately via DefinitelyTyped. The version of the library might be 1.0.0 and the types might be compatible with TypeScript 5.0. This TypeScript API really needs to have two separate versions, one for the library API version 1.0.0 and another for the TypeScript verison 5.0. If the TypeScript library is published with the same version as the JavaScript library 1.0.0 then there is no way to signal that it might not be compatible with TypeScript 4.0 for example.

Some approaches might include:

  • Having a pre-release suffix such as 1.0.0-typescript.5.0
  • Suffixing the library name foo_typescript5 version 1.0.0

None of these are great.

I propose SemVer 3 which supports multiple version axes.

One form could be: 1.0.0/5.0

This might be a bit too simplistic. How could we handle adding/removing an axis and or changing the order?

Originally created by @steinybot on GitHub (May 25, 2023). Original GitHub issue: https://github.com/semver/semver/issues/948 There are situations where an API has multiple axes for compatibility. Typical examples include: - Vendored/bundled libraries - Transpiling from one language/tool to another - Cross building for different target environments Consider a JavaScript library with TypeScript definitions where those TypeScript definitions are published separately via DefinitelyTyped. The version of the library might be 1.0.0 and the types might be compatible with TypeScript 5.0. This TypeScript API really needs to have two separate versions, one for the library API version 1.0.0 and another for the TypeScript verison 5.0. If the TypeScript library is published with the same version as the JavaScript library 1.0.0 then there is no way to signal that it might not be compatible with TypeScript 4.0 for example. Some approaches might include: - Having a pre-release suffix such as `1.0.0-typescript.5.0` - Suffixing the library name `foo_typescript5` version `1.0.0` None of these are great. I propose SemVer 3 which supports multiple version axes. One form could be: `1.0.0/5.0` This might be a bit too simplistic. How could we handle adding/removing an axis and or changing the order?
Author
Owner

@steinybot commented on GitHub (May 25, 2023):

Perhaps subsequent axes could require a label prefix such as: 1.0.0/typescript=5.0

<!-- gh-comment-id:1563637825 --> @steinybot commented on GitHub (May 25, 2023): Perhaps subsequent axes could require a label prefix such as: `1.0.0/typescript=5.0`
Author
Owner

@steinybot commented on GitHub (May 25, 2023):

Nesting could be supported with multiple / such as: 1.0.0/typescript=5.0//ecmascript=13/node=19

This would be interpreted as the following axes:

  • main=1.0.0
  • typescript=
    • main=5.0
    • ecmascript=13
  • node=19
<!-- gh-comment-id:1563640839 --> @steinybot commented on GitHub (May 25, 2023): Nesting could be supported with multiple `/` such as: `1.0.0/typescript=5.0//ecmascript=13/node=19` This would be interpreted as the following axes: - main=1.0.0 - typescript= - main=5.0 - ecmascript=13 - node=19
Author
Owner

@ljharb commented on GitHub (May 26, 2023):

I’m confused why this would be useful. In the node ecosystem, compatibility with typescript would be achieved using peerDependencies, and compatibility with node using engines.node. Ecmascript editions are a completely inappropriate target since no engine implements each yearly edition all at once.

<!-- gh-comment-id:1563647967 --> @ljharb commented on GitHub (May 26, 2023): I’m confused why this would be useful. In the node ecosystem, compatibility with typescript would be achieved using peerDependencies, and compatibility with node using engines.node. Ecmascript editions are a completely inappropriate target since no engine implements each yearly edition all at once.
Author
Owner

@steinybot commented on GitHub (May 26, 2023):

I’m confused why this would be useful.

For cross building. Any time you have a build matrix (GitHub, Travis CI, CircleCI) and maintain parallel releases of an API for different targets there is no good way to version these.

In the Ivy / Maven world people end up abusing the artifact name. Consider the Slinky library which has a two axes build matrix, one for the Scala.js version and another for the Scala version (arguably there should be a third for the React version). It ends up looking like:

<!-- https://mvnrepository.com/artifact/me.shadaj/slinky-core -->
<dependency>
    <groupId>me.shadaj</groupId>
    <artifactId>slinky-core_sjs0.6_2.13</artifactId>
    <version>1-5ee1000d</version>
</dependency>

Where sjs0.6 is Scala.js version 0.6 and 2.13 is the Scala version 2.13. See here how there are multiple "versions" for each version https://mvnrepository.com/artifact/me.shadaj/slinky-core.

In the node ecosystem, compatibility with typescript would be achieved using peerDependencies

That is true but what would your versioning be if you wanted to publish a single version of your library 1.0.0 with two versions of your types, one for an older version of TypeScript and another for a newer one? AFAIK you can't. You have one and you force your users to keep up with whatever version of TypeScript you use.

Ecmascript editions are a completely inappropriate target since no engine implements each yearly edition all at once.

That was only included to demonstrate a possible syntax.

I tried to use TypeScript as it is more relatable to a wider audience whilst being orthogonal to the real issue we are having right now with ScalablyTyped. If that example doesn't appeal then there are many more.

Vendored/bundled libraries

Often people will vendor or bundle a library within their own and include some modifications. The new library needs its own version but it also needs to take into account the version of the code it is vendoring. The build matrix would have an axis for the downstream library version. The version should be the modification version crossed with the downstream library version.

Transpiling from one language/tool to another

Consider the case of ScalablyTyped that generates Scala types from TypeScript types. Different ScalablyTyped versions can produce wildly different types and there is a set of core types. Then there are different Scala.js versions and different Scala versions. As a library author who publishes a ScalablyTyped library containing the types then there would be a build matrix with these three axes. The version is a cross product of these three axes plus the main library version.

I argue that TypeScript type library authors should be doing something similar but don't because there is no mechanism available.

Cross building for different target environments

Scala libraries are a prime example of where this is a problem. The Scala standard library is just a dependency like the TypeScript example but almost every library is published for multiple Scala versions. The build tool sbt has built-in syntax to get the right Scala version which appends the Scala binary version to the artifact name. With my proposal this would move to the version where it belongs.

<!-- gh-comment-id:1563735552 --> @steinybot commented on GitHub (May 26, 2023): > I’m confused why this would be useful. For cross building. Any time you have a build matrix ([GitHub](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs), [Travis CI](https://docs.travis-ci.com/user/build-matrix/), [CircleCI](https://circleci.com/docs/using-matrix-jobs/)) and maintain parallel releases of an API for different targets there is no good way to version these. In the Ivy / Maven world people end up abusing the artifact name. Consider the Slinky library which has a two axes build matrix, one for the Scala.js version and another for the Scala version (arguably there should be a third for the React version). It ends up looking like: ``` <!-- https://mvnrepository.com/artifact/me.shadaj/slinky-core --> <dependency> <groupId>me.shadaj</groupId> <artifactId>slinky-core_sjs0.6_2.13</artifactId> <version>1-5ee1000d</version> </dependency> ``` Where `sjs0.6` is Scala.js version 0.6 and `2.13` is the Scala version 2.13. See here how there are multiple "versions" for each version https://mvnrepository.com/artifact/me.shadaj/slinky-core. > In the node ecosystem, compatibility with typescript would be achieved using peerDependencies That is true but what would your versioning be if you wanted to publish a single version of your library `1.0.0` with two versions of your types, one for an older version of TypeScript and another for a newer one? AFAIK you can't. You have one and you force your users to keep up with whatever version of TypeScript you use. > Ecmascript editions are a completely inappropriate target since no engine implements each yearly edition all at once. That was only included to demonstrate a possible syntax. I tried to use TypeScript as it is more relatable to a wider audience whilst being orthogonal to the real issue we are having right now with [ScalablyTyped](https://scalablytyped.org/docs/readme.html). If that example doesn't appeal then there are many more. **Vendored/bundled libraries** Often people will vendor or bundle a library within their own and include some modifications. The new library needs its own version but it also needs to take into account the version of the code it is vendoring. The build matrix would have an axis for the downstream library version. The version should be the modification version crossed with the downstream library version. **Transpiling from one language/tool to another** Consider the case of ScalablyTyped that generates Scala types from TypeScript types. Different ScalablyTyped versions can produce wildly different types and there is a set of core types. Then there are different Scala.js versions and different Scala versions. As a library author who publishes a ScalablyTyped library containing the types then there would be a build matrix with these three axes. The version is a cross product of these three axes plus the main library version. I argue that TypeScript type library authors should be doing something similar but don't because there is no mechanism available. **Cross building for different target environments** Scala libraries are a prime example of where this is a problem. The Scala standard library is just a dependency like the TypeScript example but almost every library is published for multiple Scala versions. The build tool sbt has built-in syntax to [get the right Scala version](https://www.scala-sbt.org/release/docs/Library-Dependencies.html#Getting+the+right+Scala+version+with) which appends the Scala binary version to the artifact name. With my proposal this would move to the version where it belongs.
Author
Owner

@ljharb commented on GitHub (May 26, 2023):

In the specific case for TS, you may or may not be able to, but in the general case, you'd include both versions in your peer deps range, and it would Just Work. I do it all the time for react, eslint, and babel plugins.

I don't think semver is the right place to capture this information. You could certainly devise your own standard that composes multiple semver versions, of course.

<!-- gh-comment-id:1563755259 --> @ljharb commented on GitHub (May 26, 2023): In the specific case for TS, you may or may not be able to, but in the general case, you'd include both versions in your peer deps range, and it would Just Work. I do it all the time for react, eslint, and babel plugins. I don't think semver is the right place to capture this information. You could certainly devise your own standard that composes multiple semver versions, of course.
Author
Owner

@steinybot commented on GitHub (May 26, 2023):

In the specific case for TS, you may or may not be able to, but in the general case, you'd include both versions in your peer deps range, and it would Just Work. I do it all the time for react, eslint, and babel plugins.

For the situation I am describing this doesn't work. Consider where the types are using a new feature that was added in TypeScript 5.0. The version range must start at 5.0. This forces all users to upgrade to TypeScript 5.0. You could argue well then only use TypeScript features from 4.0. But that kinda sucks for users who could otherwise take advantage of the new features of 5.0. The best solution (that I know of) would be to publish two different artifacts, one for each TypeScript ^4.0.0 and another for ^5.0.0 but then how does a user specify which version they want? SemVer 2 isn't enough.

I don't think semver is the right place to capture this information. You could certainly devise your own standard that composes multiple semver versions, of course.

That is one way it could be done. It would be an extension to SemVer and use the same version field. For these reasons I think SemVer is the right place to do that.

A major downside to this approach would be that it would be beholden to any future changes that SemVer were to make. If SemVer were to start using / for example, this extension would break completely. I think this would kill any chance of it being a feasible idea. The only saving grace would be if SemVer could reserve the / character and guarantee that it never uses it itself.

Anyway this is just a very early stage idea. It would obviously need a lot of buy in from tool maintainers to make it feasible.

<!-- gh-comment-id:1563783704 --> @steinybot commented on GitHub (May 26, 2023): > In the specific case for TS, you may or may not be able to, but in the general case, you'd include both versions in your peer deps range, and it would Just Work. I do it all the time for react, eslint, and babel plugins. For the situation I am describing this doesn't work. Consider where the types are using a new feature that was added in TypeScript 5.0. The version range must start at 5.0. This forces all users to upgrade to TypeScript 5.0. You could argue well then only use TypeScript features from 4.0. But that kinda sucks for users who could otherwise take advantage of the new features of 5.0. The best solution (that I know of) would be to publish two different artifacts, one for each TypeScript `^4.0.0` and another for `^5.0.0` but then how does a user specify which version they want? SemVer 2 isn't enough. > I don't think semver is the right place to capture this information. You could certainly devise your own standard that composes multiple semver versions, of course. That is one way it could be done. It would be an extension to SemVer and use the same version field. For these reasons I think SemVer is the right place to do that. A major downside to this approach would be that it would be beholden to any future changes that SemVer were to make. If SemVer were to start using `/` for example, this extension would break completely. I think this would kill any chance of it being a feasible idea. The only saving grace would be if SemVer could reserve the `/` character and guarantee that it never uses it itself. Anyway this is just a very early stage idea. It would obviously need a lot of buy in from tool maintainers to make it feasible.
Author
Owner

@ljharb commented on GitHub (May 26, 2023):

In that case, it'd be a semver-major bump, and you'd just maintain v1 and v2 simultaneously. Semver isn't a solution for all problems, and these problems aren't something I personally think semver should solve.

<!-- gh-comment-id:1563820336 --> @ljharb commented on GitHub (May 26, 2023): In *that* case, it'd be a semver-major bump, and you'd just maintain v1 and v2 simultaneously. Semver isn't a solution for all problems, and these problems aren't something I personally think semver should solve.
Author
Owner

@steinybot commented on GitHub (May 26, 2023):

In that case, it'd be a semver-major bump, and you'd just maintain v1 and v2 simultaneously.

But then this is a problem. Remember in this example the JavaScript library and the TypeScript definitions are separate packages but the goal is to keep the main versions in sync. The JavaScript library which has no dependency on TypeScript would be v1 and the TypeScript library which only has type definitions would be v1 and v2. Surely you wouldn't suggest to do a major bump of the JavaScript library. If you did then v1 and v2 would be the same identical binary. A minor release would then result in v1.1 and v2.1 which are still identical. A major release would then have to be v3 and v4 which makes no sense.

Anyway, let's not Strawman on the TypeScript example. I concede on that. I guess people clearly do not cross build for TypeScript even though in theory they could. If you replace TypeScript for Node.js and would you find cross building for different Node versions? If not then there are plenty of other languages / environments / runtimes where this does happen.

Semver isn't a solution for all problems, and these problems aren't something I personally think semver should solve.

No of course not. In my view SemVer is all about helping to communicate and enforce API compatibility.

The version of the supported runtime etc is just as important in the compatibility story as the version of the library. Having a single version axis is insufficient. The real compatibility is more accurately described by the cartesian product of each version axis.

It is not a valid solution to simply say bump a single version. There are by definition independent (emphasis) version axes. If we could use a single version then it would be done this way but it is not. The artifact name suffix is used because we have nothing else.

The introduction to SemVer says:

... Dependency hell is where you are when version lock and/or version promiscuity prevent you from easily and safely moving your project forward.
As a solution to this problem, we propose a simple set of rules and requirements that dictate how version numbers are assigned and incremented. These rules are based on but not necessarily limited to pre-existing widespread common practices in use in both closed and open-source software...

We are still very much still living in dependency hell, even with SemVer.

I believe that this proposal is right in line with this sentiment. It is extending the rules to be based on widespread common practices, at least in the Java/Scala ecosystem.

<!-- gh-comment-id:1563972888 --> @steinybot commented on GitHub (May 26, 2023): > In that case, it'd be a semver-major bump, and you'd just maintain v1 and v2 simultaneously. But then this is a problem. Remember in this example the JavaScript library and the TypeScript definitions are separate packages but the goal is to keep the main versions in sync. The JavaScript library which has no dependency on TypeScript would be v1 and the TypeScript library which only has type definitions would be v1 and v2. Surely you wouldn't suggest to do a major bump of the JavaScript library. If you did then v1 and v2 would be the same identical binary. A minor release would then result in v1.1 and v2.1 which are still identical. A major release would then have to be v3 and v4 which makes no sense. Anyway, let's not Strawman on the TypeScript example. I concede on that. I guess people clearly do not cross build for TypeScript even though in theory they could. If you replace TypeScript for Node.js and would you find cross building for different Node versions? If not then there are plenty of other languages / environments / runtimes where this does happen. > Semver isn't a solution for all problems, and these problems aren't something I personally think semver should solve. No of course not. In my view SemVer is all about helping to communicate and enforce API compatibility. The version of the supported runtime etc is just as important in the compatibility story as the version of the library. Having a single version axis is insufficient. The real compatibility is more accurately described by the cartesian product of each version axis. It is not a valid solution to simply say bump a single version. There are by definition _independent_ (emphasis) version axes. If we could use a single version then it would be done this way but it is not. The artifact name suffix is used because we have nothing else. The introduction to SemVer says: > ... Dependency hell is where you are when version lock and/or version promiscuity prevent you from easily and safely moving your project forward. > As a solution to this problem, we propose a simple set of rules and requirements that dictate how version numbers are assigned and incremented. These rules are based on but not necessarily limited to pre-existing widespread common practices in use in both closed and open-source software... We are still very much still living in dependency hell, even with SemVer. I believe that this proposal is right in line with this sentiment. It is extending the rules to be based on widespread common practices, at least in the Java/Scala ecosystem.
Author
Owner

@jwdonahue commented on GitHub (May 26, 2023):

In the assembler/C/C++ worlds we've been dealing with these sorts of things for decades:

LibName-DOS5-x86
LibName-OS9-6809E
LibName-Z80
etc.

When the targets are different and evolve at different rates, you give them different names.

<!-- gh-comment-id:1564692444 --> @jwdonahue commented on GitHub (May 26, 2023): In the assembler/C/C++ worlds we've been dealing with these sorts of things for decades: LibName-DOS5-x86 LibName-OS9-6809E LibName-Z80 etc. When the targets are different and evolve at different rates, you give them different names.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/semver#4834