Epoch Semantic Versioning
A version is essentially a marker, a seal of the codebase at a specific point in time.
However, code is complex, and every change involves trade-offs. Describing how a change affects the code can be tricky even with natural language. A version number alone can’t capture all the nuances of a release. That’s why we have changelogs, release notes, and commit messages to provide more context.
I see versioning as a way to communicate changes to users — a contract between the library maintainers and the users to ensure compatibility and stability during upgrades. As a user, you can’t always tell what’s changed between v2.3.4 and v2.3.5 without checking the changelog. But by looking at the numbers, you can infer that it’s a patch release meant to fix bugs, which should be safe to upgrade.
In the JavaScript ecosystem, especially for packages published on npm, we follow a convention known as Semantic Versioning, or SemVer for short. A SemVer version number consists of three parts: MAJOR.MINOR.PATCH. The rules are straightforward:
MAJOR: Increment when you make incompatible API changes.
MINOR: Increment when you add functionality in a backwards-compatible manner.
PATCH: Increment when you make backwards-compatible bug fixes.
However, humans perceive numbers on a logarithmic scale. We tend to see v2.0 to v3.0 as a huge, groundbreaking change, while v125.0 to v126.0 seems a lot more trivial, even though both indicate incompatible API changes in SemVer. This perception can make maintainers hesitant to bump the major version for minor breaking changes, leading to the accumulation of many breaking changes in a single major release, making upgrades harder for users.
The reason I’ve stuck with v0.x.x is my own unconventional approach to versioning. I prefer to introduce necessary and minor breaking changes early on, making upgrades easier, without causing alarm that typically comes with major version jumps like v2 to v3. Some changes might be "technically" breaking but don’t impact 99.9% of users in practice.
There’s a special rule in SemVer that states when the leading major version is 0, every minor version bump is considered breaking. I am kind of abusing that rule to workaround the limitation of SemVer. With zero-major versioning, we are effectively abandoning the first number, and merge MINOR and PATCH into a single number (thanks to David Blass for pointing this out)
In an ideal world, I would wish SemVer to have 4 numbers: EPOCH.MAJOR.MINOR.PATCH. The EPOCH version is for those big announcements, while MAJOR is for technical incompatible API changes that might not be significant. This way, we can have a more granular way to communicate changes.
I am proposing a new versioning scheme called 🗿 Epoch Semantic Versioning, or Epoch SemVer for short. It’s built on top of the structure of MAJOR.MINOR.PATCH, extend the first number to be the combination of EPOCH and MAJOR
The format is as follows:
{EPOCH * 1000 + MAJOR}.MINOR.PATCH
EPOCH: Increment when you make significant or groundbreaking changes.
MAJOR: Increment when you make minor incompatible API changes.
MINOR: Increment when you add functionality in a backwards-compatible manner.
PATCH: Increment when you make backwards-compatible bug fixes.
For example, UnoCSS would transition from v0.65.3 to v65.3.0 (in the case EPOCH is 0). Following SemVer, a patch release would become v65.3.1, and a feature release would be v65.4.0. If we introduced some minor incompatible changes affecting an edge case, we could bump it to v66.0.0 to alert users of potential impacts. In the event of a significant overhaul to the core, we could jump directly to v1000.0.0 to signal a new era and make a big announcement.
We shouldn’t need to bump EPOCH often. It’s mostly useful for high-level, end-user-facing libraries or frameworks. For low-level libraries, they might never need to bump EPOCH at all (ZERO-EPOCH is essentially the same as SemVer).