If you’ve been following my work in open source, you might have noticed that I have a tendency to stick with zero-major versioning, like v0.x.x
. For instance, as of writing this post, the latest version of UnoCSS is v0.65.3
, Slidev is v0.50.0
, and unplugin-vue-components
is v0.28.0
. Other projects, such as React Native is on v0.76.5
, and sharp is on v0.33.5
, also follow this pattern.
如果您一直在关注我在开源方面的工作,您可能已经注意到我倾向于坚持使用零主要版本控制,例如v0.xx
。例如,在撰写本文时,UnoCSS 的最新版本是v0.65.3
, Slidev 是v0.50.0
, unplugin-vue-components
是v0.28.0
。其他项目,例如React Native v0.76.5
,sharp v0.33.5
,也遵循这种模式。
People often assume that a zero-major version indicates that the software is not ready for production. However, all of the projects mentioned here are quite stable and production-ready, used by millions of projects.
人们通常认为零主要版本表明该软件尚未准备好投入生产。然而,这里提到的所有项目都非常稳定并且可以投入生产,已被数百万个项目使用。
Why? - I bet that’s your question reading this.
为什么? - 我敢打赌这就是你读这篇文章时的问题。
Versioning 版本控制#
Version numbers act as snapshots of our codebase, helping us communicate changes effectively. For instance, we can say "it works in v1.3.2, but not in v1.3.3, there might be a regression." This makes it easier for maintainers to locate bugs by comparing the differences between these versions. A version is essentially a marker, a seal of the codebase at a specific point in time.
版本号充当代码库的快照,帮助我们有效地传达更改。例如,我们可以说“它在 v1.3.2 中有效,但在 v1.3.3 中无效,可能会出现回归”。这使得维护人员可以更轻松地通过比较这些版本之间的差异来定位错误。版本本质上是一个标记,是代码库在特定时间点的密封。
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. This ability to understand changes just by looking at the version number is possible because both the library maintainer and the users agree on the versioning scheme.
我将版本控制视为向用户传达更改的一种方式 - 库维护者和用户之间的合同,以确保升级期间的兼容性和稳定性。作为用户,如果不检查变更日志,您无法总是知道v2.3.4
和v2.3.5
之间发生了什么变化。但通过查看数字,您可以推断这是一个旨在修复错误的补丁版本,应该可以安全升级。这种仅通过查看版本号来了解更改的能力是可能的,因为库维护者和用户都同意版本控制方案。
Since versioning is only a contract, and could be interpreted differently to each specific project, you shouldn’t blindly trust it. It serves as an indication to help you decide when to take a closer look at the changelog and be cautious about upgrading. But it’s not a guarantee that everything will work as expected, as every change might introduce behavior changes, whether it’s intended or not.
由于版本控制只是一个合同,并且可以对每个特定项目进行不同的解释,因此您不应该盲目信任它。它可以作为指示,帮助您决定何时仔细查看变更日志并谨慎升级。但这并不能保证一切都会按预期进行,因为每次更改都可能会引入行为变化,无论是有意还是无意。
Semantic Versioning 语义版本控制#
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:
在 JavaScript 生态系统中,特别是对于在 npm 上发布的包,我们遵循称为语义版本控制(Semantic Versioning)或简称 SemVer 的约定。 SemVer 版本号由三部分组成: MAJOR.MINOR.PATCH
。规则很简单:
- MAJOR: Increment when you make incompatible API changes.
MAJOR :当您进行不兼容的 API 更改时递增。 - MINOR: Increment when you add functionality in a backwards-compatible manner.
MINOR :当您以向后兼容的方式添加功能时递增。 - PATCH: Increment when you make backwards-compatible bug fixes.
PATCH :当您进行向后兼容的错误修复时增加。
Package managers we use, like npm
, pnpm
, and yarn
, all operate under the assumption that every package on npm adheres to SemVer. When you or a package specifies a dependency with a version range, such as ^1.2.3
, it indicates that you are comfortable with upgrading to any version that shares the same major version (1.x.x
). In these scenarios, package managers will automatically determine the best version to install based on what is most suitable for your specific project.
我们使用的包管理器,如npm
、 pnpm
和yarn
,都是在 npm 上的每个包都遵守 SemVer 的假设下运行的。当您或包指定版本范围的依赖项(例如^1.2.3
)时,表示您愿意升级到共享相同主要版本( 1.xx
)的任何版本。在这些情况下,包管理器将根据最适合您的特定项目的版本自动确定要安装的最佳版本。
This convention works well technically. If a package releases a new major version v2.0.0
, your package manager won’t install it if your specified range is ^1.2.3
. This prevents unexpected breaking changes from affecting your project until you manually update the version range.
这个约定在技术上运作良好。如果某个软件包发布了新的主要版本v2.0.0
,并且您指定的范围是^1.2.3
,那么您的软件包管理器将不会安装它。这可以防止意外的重大更改影响您的项目,直到您手动更新版本范围。
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. Conversely, with something like v125.0
, it becomes difficult to convey the significance of a major change, as the jump to v126.0
appears minor.
然而,人类以对数尺度感知数字。我们倾向于将v2.0
到v3.0
视为一个巨大的、突破性的变化,而v125.0
到v126.0
则显得微不足道,尽管两者都表明 SemVer 中的 API 变化不兼容。这种看法可能会让维护者犹豫是否要针对较小的重大更改升级主要版本,从而导致在单个主要版本中累积许多重大更改,从而使用户升级变得更加困难。相反,对于像v125.0
这样的东西,很难传达重大变化的重要性,因为跳转到v126.0
看起来很小。
Dominik Dorfmeister had a great talk about API Design, which mentions an interesting inequality that descripting this: "Breaking Changes !== Marketing Event"
Dominik Dorfmeister做了一次关于 API 设计的精彩演讲,其中提到了一个有趣的不等式来描述这一点: “重大更改!== 营销事件”
Progressive #
I am a strong believer in the principle of progressiveness. Rather than making a giant leap to a significantly higher stage all at once, progressiveness allows users to adopt changes gradually at their own pace. It provides opportunities to pause and assess, making it easier to understand the impact of each change.

I believe we should apply the same principle to versioning. Instead of treating a major version as a massive overhaul, we can break it down into smaller, more manageable updates. For example, rather than releasing v2.0.0
with 10 breaking changes from v1.x
, we could distribute these changes across several smaller major releases. This way, we might release v2.0
with 2 breaking changes, followed by v3.0
with 1 breaking change, and so on. This approach makes it easier for users to adopt changes gradually and reduces the risk of overwhelming them with too many changes at once.

Leading Zero Major Versioning #
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. (Breaking changes are relative. Even a bug fix can be breaking for those relying on the previous behavior, but that’s another topic for discussion :P).
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):
ZERO.MAJOR.{MINOR + PATCH}
Of course, zero-major versioning is not the only solution to be progressive. We can see that tools like Node.js, Vite, Vitest are rolling out major versions in consistent intervals, with a minimal set of breaking changes in each release that are easy to adopt. It would require a lot of effort and extra attentions. Kudos to them!
I have to admit that sticking to zero-major versioning isn’t the best practice. While I aimed for more granular versioning to improve communication, using zero-major versioning has actually limited the ability to convey changes effectively. In reality, I’ve been wasting a valuable part of the versioning scheme due to my peculiar insistence.
Thus, here, I am proposing to change.
Epoch Semantic Versioning #
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. Similar we also have Romantic Versioning that propose HUMAN.MAJOR.MINOR
. But, of course, it’s too late for the entire ecosystem to adopt a new versioning scheme.
If we can’t change SemVer, maybe we can at least extend it. 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
. To put a difference between them, we use a third digit to represent EPOCH
, which gives MAJOR
a range from 0 to 99. This way, it follows the exact same rules as SemVer without requiring any existing tools to change, but provides more granular information to users.
The name "Epoch" is inspired by Debian’s versioning scheme.
The format is as follows:
{EPOCH * 100 + 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 v100.0.0
to signal a new era and make a big announcement. I’d suggest assigning a code name to each non-zero EPOCH
to make it more memorable and easier to refer to. This approach provides maintainers with more flexibility to communicate the scale of changes to users effectively.
We shouldn’t need to bump
EPOCH
often. And it’s mostly useful for high-level, end-user-facing libraries or frameworks. For low-level libraries, they might never need to bumpEPOCH
at all (ZERO-EPOCH
is essentially the same as SemVer).
Of course, I’m not suggesting that everyone should adopt this approach. It’s simply an idea to work around the existing system, and only for those packages with this need. It will be interesting to see how it performs in practice.
Moving Forward #
I plan to adopt Epoch Semantic Versioning in my projects, including UnoCSS, Slidev, and all the plugins I maintain, and ultimately abandon zero-major versioning for stable packages. I hope this new versioning approach will help communicate changes more effectively and provide users with better context when upgrading.
I’d love to hear your thoughts and feedback on this idea. Feel free to share your comments using the links below!