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

Support multiple version axes #948

Open
steinybot opened this issue May 25, 2023 · 9 comments
Open

Support multiple version axes #948

steinybot opened this issue May 25, 2023 · 9 comments

Comments

@steinybot
Copy link

steinybot commented May 25, 2023

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?

@steinybot steinybot changed the title Support multiple version axis Support multiple version axes May 25, 2023
@steinybot
Copy link
Author

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

@steinybot
Copy link
Author

steinybot commented 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

@ljharb
Copy link
Contributor

ljharb commented 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.

@steinybot
Copy link
Author

steinybot commented 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.

@ljharb
Copy link
Contributor

ljharb commented 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.

@steinybot
Copy link
Author

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.

@ljharb
Copy link
Contributor

ljharb commented 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.

@steinybot
Copy link
Author

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.

@jwdonahue
Copy link
Contributor

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 up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants
@ljharb @steinybot @jwdonahue and others