前言
保姆级对于vue3的版本发布流程解读。 详细解答都在每一行代码的注释里面,主要看注释。 学完后可以搭建自己的release流程了(再也不是手动挡了)。 文档较长,建议分次阅读并自己建一个走一遍。
准备
- 准备node-10+环境以及yarn1.x
- 克隆vue3_release学习仓库代码项目
- cd vue3_release
- yarn 安装依赖
- yarn release就可以控制台体验999等级的输出了
先上效果图:
阅读
整个文件分为两个部分阅读:
- 依赖以及辅助函数
- main进程
依赖以及辅助函数
args(获取命令行参数)
const args = require('minimist')(process.argv.slice(2))
// 命令行参数
// 例子:
// process.argv:
// 第一个是node路径
// 第二是执行脚步路径
// 后面是参数,所以去除两个路径
// node release.js -name zhou
// [
// 3:'zhou'
// 2:'-name'
// 1:'/Users/zhouguang/Desktop/learn/vue/源码阅读/release/release.js'
// 0:'/usr/local/bin/node'
// ]
// args: minimist转换后的参数对象
// { _: [], name: 'zhou' }
依赖包
const fs = require('fs')
// 文件操作,用于读取
const path = require('path')
// 路径解析
const chalk = require('chalk')
// 彩色终端
const semver = require('semver')
// 版本号生成和对比
const currentVersion = require('../package.json').version
// 现在的版本号
const { prompt } = require('enquirer')
// 命令行选择交互
const execa = require('execa');
// 子进程,在终端执行命令
// 例子:
// (async () => {
// const {stdout} = await execa('echo', ['zhou'])
// console.log(stdout) // zhou
// })();
preId(版本后缀类型)
const preId = args.preid || (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
// 获取preid
// 命令行参数的发布版本的后缀,或者是当前版本的后缀1.0.0-alpha.x => alpha
isDryRun(是否只log不执行,简称空运行)
const isDryRun = args.dry
// 命令行参数控制是否空运行,打开方式:node release.js --dry
// 本项目package.json中release命令默认携带
skipTests(跳过测试)
const skipTests = args.skipTests
// 跳过测试,打开方式:node release.js --skipTests
skipBuild(跳过构建)
const skipBuild = args.skipBuild
// 跳过构建,打开方式:node release.js --skipBuild
packages(子包名称数组集合)
获取在scripts同层packages文件夹下子包名称的数组集合
const packages = fs
.readdirSync(path.resolve(__dirname, '../packages'))
.filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
// 获取package下非ts结尾并且不是.开头的文件或者文件夹名称
skippedPackages(需要跳过的包)
const skippedPackages = []
// 跳过的包
versionIncrements(版本升级类型数组)
const versionIncrements = [
'patch',
'minor',
'major',
...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]
// 版本号类型数组,有preId则把后缀类型加入
inc(生成版本号函数)
const inc = i => semver.inc(currentVersion, i, preId)
// 生成版本号函数
// 例子
// semver.inc('1.0.0', 'patch', 'beta') => 1.0.1
// semver.inc('1.0.0', 'minor', 'beta') => 1.1.0
// semver.inc('1.0.0', 'major', 'beta') => 2.0.0
// pre就是增加后缀
// semver.inc('1.0.0', 'major', 'beta') => 2.0.0-beta.0
// prerelease增加后缀的版本号
// semver.inc('1.0.0-beta.0', 'prerelease', 'beta') => 1.0.0-beta.1
bin(生成命令执行函数)
const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 生成命令执行函数
// 利用node_modules下的bin,相当于bin(webapck) => node_modules/.bin/webpack
run(终端执行命令运行函数)
const run = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
// 终端执行命令运行函数,对execa进行二次封装
dryRun(空运行函数)
const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 空运行函数,使用chalk进行打印要执行的命令
runIfNotDry(执行判断函数)
const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 执行判断函数,利用外部isDryRun变量判断要进行哪个执行
getPkgRoot(获取单个包的路径函数)
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
// 获取单个包的路径函数
step(终端console)
const step = msg => console.log(chalk.cyan(msg))
// 终端彩色console,默认蓝色哦
main进程
直接跳到main函数进行阅读即可,main相关操作函数放置在main前面(个人习惯,哈哈),vue源码是放在main后面。
1. 生成版本号
取出命令行内的版本号
let targetVersion = args._[0]
// 取出命令行内的版本号,例如node release 3.1.2
// args._一个包含未指定参数内容组成的数组
交互生成版本号
if (!targetVersion) {
// 未指定版本号
const { release } = await prompt({
type: 'select',
name: 'release',
message: 'Select release type',
choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
})
// 命令交互选择版本类型 例子:minor (1.0.1)
if (release === 'custom') {
// 自定义输入版本号
targetVersion = (
await prompt({
type: 'input',
name: 'version',
message: 'Input custom version',
initial: currentVersion
})
).version
} else {
targetVersion = release.match(/\((.*)\)/)[1]
// 取出release中版本号 例子:minor (1.0.1) => 1.0.1
}
}
校验版本号是否合法
// 校验版本号是否符合标准
if (!semver.valid(targetVersion)) {
throw new Error(`invalid target version: ${targetVersion}`)
}
交互确认用户是否要发布这个版本
const { yes } = await prompt({
type: 'confirm',
name: 'yes',
message: `Releasing v${targetVersion}. Confirm?`
})
// 不是直接退出
if (!yes) {
return
}
2. jest执行测试
step('\nRunning tests...')
// 终端输出正在运行测试
if (!skipTests && !isDryRun) {
// 非跳过测试以及非空运行才运行测试
await run(bin('jest'), ['--clearCache'])
await run('yarn', ['test', '--bail'])
} else {
console.log(`(skipped)`)
}
3. 更新vue以及所有相关包的版本
step('\nUpdating cross dependencies...')
// 终端输出更新所有包版本中
updateVersions(targetVersion)
// 执行更新函数
updateVersions(更新相关包版本)
当前项目中packages中建立了package1和2两个示例, 它们的版本号会一起变更
// 更新版本号
function updateVersions (version) {
// 1. 更新根目录package.json
updatePackage(path.resolve(__dirname, '..'), version)
// 2. 更新packages下所有package.json
packages.forEach(p => updatePackage(getPkgRoot(p), version))
}
updatePackage(更新单个包)
作用是更新三个地方的version
- package.json的version
- dependencies内和vue相关包的版本号
- peerDependencies内和vue相关包的版本号
// 更新package
function updatePackage(pkgRoot, version) {
// 获取路径下的package.json
const pkgPath = path.resolve(pkgRoot, 'package.json')
// 读取package.json并转换json对象
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
// 更新version
pkg.version = version
// 更新dependencies内和vue相关包的版本号
// dependencies:作为依赖安装的 npm 软件包的列表
updateDeps(pkg, 'dependencies', version)
// 更新peerDependencies内和vue相关包的版本号
// peerDependencies:[对等依赖](https://nodejs.org/en/blog/npm/peer-dependencies/),当前包使用必须的其它依赖包
updateDeps(pkg, 'peerDependencies', version)
// 新数据写入package.json
// JSON.stringify(json对象, 可选:序列化格式函数, 可选:缩进空白字符个数)
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
updateDeps(更新查找到的vue相关包的版本号)
function updateDeps(pkg, depType, version) {
// 获取依赖数组
const deps = pkg[depType]
// 没有则返回
if (!deps) return
// 循环对比依赖是否符合以下两个条件,符合则更新版本
// 1。=== vue
// 2.@vue开头,并且剔除@vue/后的内容在packages内
Object.keys(deps).forEach(dep => {
if (
dep === 'vue' ||
(dep.startsWith('@vue') && packages.includes(dep.replace(/^@vue\//, '')))
) {
console.log(
chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`)
)
deps[dep] = version
}
})
}
chalk打印出所有更新相关包的对应依赖的版本
4. 构建所有包
step('\nBuilding all packages...')
// 终端输出构建所有包中
if (!skipBuild && !isDryRun) {
// 非跳过测试以及非空运行才运行构建
await run('yarn', ['build', '--release'])
// 运行构建
step('\nVerifying type declarations...')
// 终端输出校验TS类型声明
await run('yarn', ['test-dts-only'])
// 执行校验TS类型声明
} else {
console.log(`(skipped)`)
}
5. 生成changelog说明md文件
借助conventional-changelog-cli将git commit history中符合angular规范的整合输出changelog.md文件
// 执行conventional-changelog -p angular -i CHANGELOG.md -s
// 依赖包:conventional-changelog-cli
// 按照angular规范生成changelog文件,即版本下列出fix,feat,revert的更新
await run(`yarn`, ['changelog'])
6. git提交commit
const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
// 命令行执行git diff,检查是否有更改
if (stdout) {
// 如果有改动
step('\nCommitting changes...')
// 终端输出git commit中
await runIfNotDry('git', ['add', '-A'])
// 提交到git暂存区
await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`])
// git commit release: v1.0.1,生成版本commit信息
} else {
console.log('No changes to commit.')
}
7. yarn发布包
step('\nPublishing packages...')
// 终端输出包发布中
// 循环packages中的包并发布
for (const pkg of packages) {
await publishPackage(pkg, targetVersion, runIfNotDry)
}
publishPackage(发布包
async function publishPackage(pkgName, version, runIfNotDry) {
// 如果包含在跳过包名单里,则跳过
if (skippedPackages.includes(pkgName)) {
return
}
// 根据包名组合成包的文件夹路径
const pkgRoot = getPkgRoot(pkgName)
// 包路径+package.json组成完整路径
const pkgPath = path.resolve(pkgRoot, 'package.json')
// fs读取包下面的package.json内容并json对象化
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
// 私有包则跳过
if (pkg.private) {
return
}
// For now, all 3.x packages except "vue" can be published as
// `latest`, whereas "vue" will be published under the "next" tag.
// 上面的意思是vue3.x 发布为 next标签,对应安装vue3时是 npm i vue@next
// 发布的标签
let releaseTag = null
if (args.tag) {
// 命令行如果有,则用命令行的,例子:node release --tag alpha
releaseTag = args.tag
} else if (version.includes('alpha')) {
releaseTag = 'alpha'
} else if (version.includes('beta')) {
releaseTag = 'beta'
} else if (version.includes('rc')) {
releaseTag = 'rc'
} else if (pkgName === 'vue') {
// TODO remove when 3.x becomes default
// 当vue3变成默认版本时,则去除next标签,安装就变成 npm i vue
releaseTag = 'next'
}
// TODO use inferred release channel after official 3.0 release
// const releaseTag = semver.prerelease(version)[0] || null
// 后面计划用semvers生成tag
step(`Publishing ${pkgName}...`)
// 终端输出 发布xxx包中
// yarn发布包
try {
await runIfNotDry(
'yarn',
[
'publish',
'--new-version',
version,
...(releaseTag ? ['--tag', releaseTag] : []),
'--access',
'public'
],
{
cwd: pkgRoot,
stdio: 'pipe'
}
)
console.log(chalk.green(`Successfully published ${pkgName}@${version}`))
} catch (e) {
if (e.stderr.match(/previously published/)) {
console.log(chalk.red(`Skipping already published: ${pkgName}`))
} else {
throw e
}
}
}
8. 打标签并推送远程仓库
step('\nPushing to GitHub...')
// 终端输出推送到github中
await runIfNotDry('git', ['tag', `v${targetVersion}`])
// git tag v1.0.0 => git打标签
await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
// git push origin refs/tags/v1.0.0 => git推送标签到远程仓库
await runIfNotDry('git', ['push'])
// git push => git提交
9. 结束log
// 如果是空运行,则打印空运行结束,可以使用git diff查看包的变化
if (isDryRun) {
console.log(`\nDry run finished - run git diff to see package changes.`)
}
// 如果有跳过包名单,则终端黄色打印跳过包名单
if (skippedPackages.length) {
console.log(
chalk.yellow(
`The following packages are skipped and NOT published:\n- ${skippedPackages.join(
'\n- '
)}`
)
)
}
执行main
// 执行main
main().catch((err) => console.error(err))
总结
- 辅助函数内包装依赖包API使用,并且语义化,非常利于写业务进程时辅助使用,用起来。栗子:
const run = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
// 终端执行命令运行函数,对execa进行二次封装
const dryRun = (bin, args, opts = {}) =>
console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
// 空运行函数,使用chalk进行打印要执行的命令
const runIfNotDry = isDryRun ? dryRun : run
// 执行判断函数,利用外部isDryRun变量判断要进行哪个执行
- package.json中peerDependencies(对等依赖)了解,官方文档
- 终端交互式发布流程,真香。
参考
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!