依赖版本有价值的前提,是发布者与使用者对公共契约有共同认识。语义化版本为变化提供一套表达:从 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。输出保留版本、范围与布尔结果,便于逐项看清究竟是哪条条件在起作用。
...