ui、js-sdk,cli等库的研发流程中,发布 npm 版本,有一个麻烦问题——设置 npm 的 version。
网上讨论 npm pkg 版本设置,基本都是手动,我尝试过,觉得很麻烦,本文将给出一种自动设置 npm version 版本方案。
首先分析一下 设置 npm 的 version 麻烦的原因。 如果不想看看文章,直接看我实现的 代码地址
一、麻烦的原因有哪些?
原因1 学习成本较高,不常用
npm version --help 我可以查看到,有下面这些参数,都是用来设置版本的。
npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=<prerelease-id>] | from-git]
版本格式定义为 x.y.z-t,x、y、z、t 的数值增加,可以找到上面对应参数,这还不止,如果在 prereleade 设置 --preid=alpha 或者 --preid=beta,学习成本是有一些。
想了解版本相关知识,可以看看Semantic Versioning 2.0.0,研发各种包、游戏的版本概念都是根据这个来的。
原因2 设置版本复杂
我们看下设置版本步骤
1、查看最后一个版本
# 版本太多, npm view <pkg_name> versions ,命令行输出末尾出现 ...more,看不到最后一个版本
npm view <pkg_name> versions
# 查看所有版本,输出json格式,命令行输出末尾出现不会出现...more,看到最后一个版本
npm view <pkg_name> versions --json
# 查看正式版本,不包含预发版本
npm show <pkg_name> version
查看一个版本号,也不是那么容易。
2、计算版本
提前说明:
- 由于 npm 包,它是没有生产测试环境一说,通常来说,把预发版本当成测试,正式版本当成生产。
- 这里提到预发版本,只考虑 类似
1.0.0-1
假设当前分支 npm 版本是1.0.0,远程分支 1.1.0
- 如果想设置修复版本,先设置修复预发版本1.1.1-0、1.1.1-1、1.1.1-1……,如果修复版本测试通过,设置正式版本 1.1.1
- 如果想设置次要版本,先设置次要预发版本1.2.0-0、1.2.0-1、1.2.0-1……,如果修复版本测试通过,设置正式版本 1.2.0
怎么判断是修复版本、还是次要版本了?通常来说增加一个功能模块,算是次要版本,如果不新增,只是对原有模块的bug修复或者优化,可以使用次要版本。
主要版本(major )设置,不是常态,一旦设置主要,整个仓库都要改动,自动不一定好。
3、设置版本
npm version [newVersion] 有坑,执行这条命名,自动帮你设置 git tag。虽然加上 --no-git-tag-version 能够解决。
为什么执行设置版本命令不需要设置git tag? 是因为如果有多个人开发,某个开发的当前分支,没有最全部的代码,突然设置版本,打了一个git tag,这显然不合理。
总结一下原因
你看,仅仅设置一个小小 npm version,要考虑这么多东西,成本不小。
二、编写自动设置 npm pkg 版本脚本
思考哪些版本自动设置?
-
主要版本设置,暂时不考虑
-
次要版本设置、修复版本设置、次要预发版本设置、修复预发版本设置,需要考虑设置
思考如何判断次要版本,还是修复版本?
这个是动设置 npm pkg 版本的关键步骤。
通常来说,次要版本也是功能版本,新增一个功能。反应代码层面上,就是新增一个目录。
举个例子,下面是一个 js-sdk 的源码
新增 module1或这 module2 这个时候就应该设置 预发版本。否则就是次要版本。
|__ src
|__ module1
| |__index.js
| |__readme.md
|__ module2
|__index.js
|__readme.md
所以,可以得到这么一个思路:
- 将当前分支和远程master分支进行 diff,拿到当前分支下,src 目录哪些子目录有变动,得到一个新目录列表。
- 获取远程master 分支的 src 的目录旧子目录列表
- 2者一比,如果一样,就得设置修复版本。如果不一样,有新增的模块,就得设置次要版本
具体代码实现,有2种
- 第1种,用 nodegit,但需要凭据,就是远程仓库的密码,由于这一点,我直接放弃了,设置一个版本还是输入密码帐号,不扯蛋。
- 第2种,用执行 shelljs 执行 git 相关命令,做字符串处理。(git 官方文档,不是人看得懂的,相关 git 命令不好找,但是有了 chatgtp 没什么不好找)
由于上面一些原因,技术选择就第 2 种,实现代码如下:
build/npm/setVersion/autoSetVersion.js
const { execSync } = require('child_process')
const setPreYVersion = require('./setPreYVersion')
const setPreZversion = require('./setPreZVersion')
const __MASTER_BRANCH__ = 'origin/master'
const __MODULE_PATHS__ = [
'src'
]
function getAddedModuleFolder() {
// 获取当前分支
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
// 获取当前分支相对于远程 master 分支的差异
const diffOutput = execSync(`git diff --name-status origin/master...${currentBranch}`).toString().trim();
// 获取新增的子目录和文件
const addedPaths = diffOutput
.split('\n')
.filter(line => line.startsWith('A\tsrc/'))
.map(url => {
const separator = '/'
// 查找第二个分隔符的位置
const secondSeparatorIndex = url.indexOf(separator, url.indexOf(separator) + 1);
// 截取前两层目录
const result = url.slice(0, secondSeparatorIndex)
return result.replace(/^A\t/, '')
})
// 获取远程 `master` 分支上的src所有子目录
// 获取远程分支的目录结构
function getRemoteBranchDirectoryStructure(remoteBranch, directoryPath) {
try {
const command = `git ls-tree --name-only ${remoteBranch}:${directoryPath}`;
const result = execSync(command, { encoding: 'utf-8' });
const directories = result.trim().split('\n');
return directories.map(url => `src/${url}`)
} catch (error) {
console.error('Error:', error.message);
return [];
}
}
// 判断指定的子目录是否存在于远程 `master` 分支
function isSubdirectoryInRemoteMaster(subdirectory) {
const remoteMasterSubdirectories = []
__MODULE_PATHS__.forEach(url, function () {
remoteMasterSubdirectories.push(getRemoteBranchDirectoryStructure(__MASTER_BRANCH__, url))
})
return remoteMasterSubdirectories.includes(subdirectory);
}
// 去重
const _addedPaths = [...new Set(addedPaths)]
.filter(url => !isSubdirectoryInRemoteMaster(url))
console.log('新增目录', _addedPaths)
return _addedPaths
}
function setVersion() {
const addedModules = getAddedModuleFolder()
// 如果是新增模块
if (addedModules.length > 0) {
setPreYVersion()
} else { // 如果修改现有的模块
setPreZversion()
}
}
setVersion()
设置版本
在上面 原因2 设置版本复杂 ,已经说了设置版本逻辑,这里不重复了,直接看待代码,下面是次要版本设置代码(修复版本设置代码逻辑是一样的)
build/npm/setVersion/autoSetVersion.js
const execShell = require('../../execShell')
const getLastInAllVersion = require('./getLastInVersions')
const pkg = require('../../../package.json')
const semver = require('semver')
function setPreYVersion () {
// 本地更新到最新版本
const latestVersion = getLastInAllVersion()
if (semver.gt(pkg.version, latestVersion)) {
return
} else if (semver.lt(pkg.version, latestVersion)) {
execShell(`npm version ${latestVersion} --no-git-tag-version`)
}
if (latestVersion.includes('-')) {
execShell(`npm version prerelease --no-git-tag-version`)
} else {
execShell(`npm version preminor --no-git-tag-version`)
}
}
module.exports = setPreYVersion
如何自动
预发版本设置的自动化
利用 git hooks中 pre-push 钩子实现(pre-push有现成npm包),用户在push的时候,就可以自动设置npm版本,默认提交一个 commit。具体看 package.json 的配置
{
"scripts": {
+ "autoSetVersion": "node ./build/npm/setVersion/autoSetVersion.js"
},
+ "pre-push": [
+ "autoSetVersion"
]
}
正式版本设置的自动化
这个要结合CI/CD,很可惜 gitee 没有。只能提供思路,当我们将代码合并到 release 或 master 后,触发对应的 hook,执行设置正式版本脚本,随表执行 npm publish
build/npm/setVersion/setOfficialVersion.js
const execShell = require('../../execShell')
const getLastInAllVersion = require('./getLastInVersions')
function setOfficialVersion () {
const latestVersion = getLastInAllVersion()
const newVersion = latestVersion.split('-')[0]
execShell(`npm version ${newVersion} --no-git-tag-version`)
}
setOfficialVersion()
(完)