依赖版本有价值的前提,是发布者与使用者对公共契约有共同认识。语义化版本为变化提供一套表达:从 1.0.0 开始,不兼容的公共接口变化需要提升主版本,兼容的功能增加需要提升次版本,兼容的问题修复需要提升补丁版本。数字归纳的是发布者对这一版的判断,并不会自行检查实现,也不能保证应用升级之后必然正确运行。理解承诺边界,比只记住三个数字更有用。

先声明兼容性覆盖哪些内容

公共接口可以包括导出的函数、正式说明的命令参数、配置格式、响应字段和明确约定的错误行为。边界应写得足够清楚,才能判断一次变化是否违约。保持契约不变的内部重构,与删除已经承诺支持的参数,不属于同一类变化。修正原先违反文档契约的行为,可能仍是兼容的缺陷修复,即便有人曾经观察并依赖那个错误表现。仍需在变更说明中解释,并验证受影响的调用方。

语义化版本规范要求:把公共功能标记为废弃时,应提升次版本;之后真正移除功能,则需要按不兼容变化处理。已经发布的某个版本,其内容不应继续变化,修正应通过另一个新版本交付。在相同版本标签背后替换文件,会破坏追溯:两个依赖声明完全相同的安装过程,可能得到不同的字节,排查问题时也无法再靠版本号明确区分。

主版本为零表示初始开发阶段,公共接口不应被视为稳定。维护者可以为这一阶段声明额外约定,但使用者不能默认所有零主版本更新都兼容。预发布版本的优先级低于对应的正式版本;加号之后的构建元数据不参与优先级比较。因此,两个版本在排序意义上相等,不意味着它们的包内容、来源或文件摘要相同。兼容性、选择顺序与产物身份是不同问题。

坏掉的样子

标错的次版本即使满足依赖范围,也可能破坏已声明的契约。标号正确的发布,也可能暴露应用对内部实现细节的依赖。升级时仍应读变更说明,运行有代表性的集成验证;需要重复解析依赖时保留锁文件。锁文件记录当时选择了哪些版本,不能替这些版本证明行为正确,更不是来源真实性的证明。版本范围允许安装,只说明解析条件满足了。

包管理器的范围语法又是另一层规则。语义化版本规范定义版本及优先级,具体工具决定约束怎样筛选候选。在 node-semver 中,插入符通常允许最左非零分量右侧的变化,所以 ^0.2.3 不接受 0.3.0,^0.0.3 也不接受 0.0.4。三个分量完整的波浪号范围,通常只允许同一次版本线内的补丁更新。不能把所有插入符都理解成“只要主版本不变就行”。

预发布筛选还有附加规则。宽泛的稳定版本范围,不会自动接纳数值更高的测试版;明确允许某个预发布三元组后,才可能接纳该三元组后续的预发布版本。应验证实际解析器,而不是用普通字符串排序推断升级关系,也不要把数值更大直接等同于可被当前范围选择。不同生态的约束语法不能仅凭相似外观相互套用。

快速验证

本轮安装尝试在解析器断言执行之前以失败状态退出;这里没有把现有其他版本库的行为算成该固定版本示例的通过证据。

将代码保存为 semver.py,在已有 Node 和 npm 的环境执行 python3 semver.py。实验需要访问公共 npm 仓库。脚本在临时目录安装 semver@7.7.2,隔离配置和缓存,关闭包安装脚本,再调用这个库真实的比较函数。它不会修改当前项目的依赖声明或锁文件;结束时临时安装目录随上下文清理。

from pathlib import Path
from tempfile import TemporaryDirectory
import json
import os
import subprocess

with TemporaryDirectory() as directory:
    root = Path(directory)
    user_config = root / 'user.npmrc'
    global_config = root / 'global.npmrc'
    user_config.write_text('')
    global_config.write_text('')
    env = {'PATH': os.environ['PATH'], 'LC_ALL': 'C',
           'NPM_CONFIG_USERCONFIG': str(user_config),
           'NPM_CONFIG_GLOBALCONFIG': str(global_config),
           'NPM_CONFIG_CACHE': str(root / 'cache')}
    subprocess.run(['npm', 'install', '--ignore-scripts', '--no-audit', '--no-fund',
                    '--package-lock=false', '--registry=https://registry.npmjs.org',
                    'semver@7.7.2'], cwd=root, env=env, check=True, capture_output=True)
    script = r'''
const s = require('semver');
const assert = require('node:assert/strict');
const cases = [
  ['1.9.0', '^1.2.3', true], ['2.0.0', '^1.2.3', false],
  ['1.2.9', '~1.2.3', true], ['1.3.0', '~1.2.3', false],
  ['0.2.9', '^0.2.3', true], ['0.3.0', '^0.2.3', false],
  ['0.0.4', '^0.0.3', false], ['1.3.0-beta.1', '^1.2.3', false],
  ['1.3.0-beta.2', '^1.3.0-beta.1', true]
].map(([version, range, expected]) => {
  const accepted = s.satisfies(version, range);
  assert.equal(accepted, expected);
  return {version, range, accepted};
});
assert.equal(s.compare('1.2.3+build.1', '1.2.3+build.2'), 0);
assert.equal(s.lt('1.3.0-beta.2', '1.3.0'), true);
assert.equal(s.valid('1.02.3'), null);
console.log(JSON.stringify({library: require('semver/package.json').version, cases,
  metadata_precedence_equal: true, prerelease_before_release: true, leading_zero_rejected: true}));
'''
    result = subprocess.run(['node', '-e', script], cwd=root, env=env,
                            capture_output=True, text=True, check=True)
    print(json.dumps(json.loads(result.stdout)))

九个范围案例应当全部符合断言:^1.2.3 接受 1.9.0,拒绝 2.0.0;波浪号拒绝下一次版本;两个零主版本范围更加收敛。稳定范围拒绝 1.3.0-beta.1,明确指定预发布的范围则接受 1.3.0-beta.2。输出保留版本、范围与布尔结果,便于逐项看清究竟是哪条条件在起作用。

其余断言准备检查两份构建元数据的优先级相等、测试版排在相应正式版之前,以及 1.02.3 这种带前导零的版本无效。完整运行通过后也只覆盖解析与选择规则,不验证某个真实依赖升级的兼容性;本轮没有执行到这些断言。决定版本提升幅度时,仍需把公共接口的实际变化和受影响调用方放在一起审阅。版本号负责传达判断,变更说明与测试负责让别人能够核对这个判断。

延伸阅读

来源

  • Semantic Versioning 2.0.0,公共接口承诺、版本优先级和初始开发规则,已核验 2026-10-15。
  • npm: node-semver,插入符、波浪号及预发布范围的实际语义,已核验 2026-10-15。