1. 前言
1.1 学习内容
本文涉及vue-next/scripts/release.js文件,
1.2 环境
- 操作系统: macOS 12.0.1
- 浏览器: Chrome 94.0.4606.81
- shell: zsh
- node 16.13.0
- vue 3.2.21
-
1.3 get知识点
熟悉 vuejs 发布流程
- 学会调试 nodejs 代码
- 动手优化公司项目发布流程
1.4 发布流程
2 环境准备
打开项目,可以通过README.md和CHANGELOG.md看到项目相关信息2.1 严格校验使用yarn安装依赖
看看package.json文件
这个是包安装时的前置钩子,在安装包之前会去运行该脚本"preinstall": "node ./scripts/preinstall.js"
``javascript // 判断文件是不是存在pnpm,如果不是,结束进程,报错 if (!/pnpm/.test(process.env.npm_execpath || '')) { console.warn(
\u001b[33mThis repository requires using pnpm as the package manager+
for scripts to work properly.\u001b[39m\n` ) process.exit(1) }
vue-next 3.2.4的版本判断的包工具是yarn,现在切换到了pnpm,node版本也切换到了16<br />其他相关[npm钩子](https://docs.npmjs.com/cli/v6/using-npm/scripts)
<a name="lLi8y"></a>
## 2.2 调试 vue-next/scripts/release.js 文件
package.json文件,在脚本的后面加上了--dry,
```javascript
// --dry 不执行测试和编译 、不执行 推送git等操作
// 只是打印,后文再详细讲述
"release": "node scripts/release.js --dry"
vscode点击dubug就可以开始调试了,命令行会出现debugger attached字段,左侧call Stack会出现node进程和release.js文件,这就开启了正常的调试
3 开始
3.1 release.js引入的包
通过调试,运行命令 node scripts/release.js --dry
// minimist获取命令行参数
const args = require('minimist')(process.argv.slice(2)) // {_: Array(0), dry: true}
// process.argv.slice(2)截取从下标2开始的数组,因为前面两个值是node的绝对路径和release.js文件的绝对路径,影响运行
const fs = require('fs')
const path = require('path')
// 命令行console添加颜色
const chalk = require('chalk')
// 版本管理
const semver = require('semver')
// 获取当前版本
const currentVersion = require('../package.json').version
// 交互式询问 CLI, prompt异步函数
const { prompt } = require('enquirer')
// 执行脚本
const execa = require('execa')
3.1.1 minimist 获取命令行参数
npm包 minimist
// 使用说明
$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo', 'bar', 'baz' ],
x: 3,
y: 4,
n: 5,
a: true,
b: true,
c: true,
beep: 'boop' }
3.1.2 chalk命令行log添加颜色
chalk.red('Hello', chalk.underline.bgBlue('world') + '!')
3.1.3 semver 包的版本管理
const semver = require('semver')
semver.valid('1.2.3') // '1.2.3'
semver.valid('a.b.c') // null
semver.clean(' =v1.2.3 ') // '1.2.3'
semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true
semver.gt('1.2.3', '9.8.7') // false
semver.lt('1.2.3', '9.8.7') // true
semver.minVersion('>=1.0.0') // '1.0.0'
semver.valid(semver.coerce('v2')) // '2.0.0'
semver.valid(semver.coerce('42.6.7.9.3-alpha')) // '42.6.7'
3.1.4 enquirer 交互式询问 CLI
// 支持选择,输入,确定等cli交互,非常方便
const { prompt } = require('enquirer')
const response = await prompt([
{
type: 'input',
name: 'name',
message: 'What is your name?'
},
{
type: 'input',
name: 'username',
message: 'What is your username?'
}
]);
console.log(response); // { name: 'Edward Chan', username: 'edwardmchan' }
3.1.5 execa 执行命令
import {execa} from 'execa';
const {stdout} = await execa('echo', ['unicorns']);
console.log(stdout);
//=> 'unicorns'
3.2 release.js前置配置参数和方法
// 上一次预发布变量,自定义参数,或者package.version的版本,例如 3.22.4-beta => beta,没有返回null
const preId =
args.preid ||
(semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
// 干运行,不真正很执行代码
const isDryRun = args.dry
// 跳过测试
const skipTests = args.skipTests
// 跳过打包
const skipBuild = args.skipBuild
// 拿到packages下面的文件夹,过滤非ts
const packages = fs
.readdirSync(path.resolve(__dirname, '../packages'))
.filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
// 跳过packages
const skippedPackages = []
// 版本递增
const versionIncrements = [
// 补丁
'patch',
// 小版本
'minor',
// 重大版本
'major',
// 如果是预发布环境,增加预发布的版本
...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]
// 生成递增版本, (当前版本,版本key(patch等),'beta')
const inc = i => semver.inc(currentVersion, i, preId)
// 获取node_modules/.bin下面的脚本
const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 执行bin脚本
const run = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
// 空运行,只打印
const dryRun = (bin, args, opts = {}) =>
console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
// 相当于包装了一层run,判断执行哪个run函数,空运行还是真实运行
const runIfNotDry = isDryRun ? dryRun : run
// 获取packages跟路径
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
// 打印下一步
const step = msg => console.log(chalk.cyan(msg))
3.3 main()函数
3.3.1 主流程
// 发布前运行测试
// run tests before release
step('\nRunning tests...')
// 更新所有包的依赖版本
// update all package versions and inter-dependencies
step('\nUpdating cross dependencies...')
// 打包所有包的类型
// build all packages with types
step('\nBuilding all packages...')
// 输出测试文件
// test generated dts files
step('\nVerifying type declarations...')
// 生成变更日志
// generate changelog
step('\nGenerating changelog...')
// 更新pnpm锁
// update pnpm-lock.yaml
step('\nUpdating lockfile...')
// 如果有git diff,
const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
if (stdout)
step('\nCommitting changes...')
// 发布包
// publish packages
step('\nPublishing packages...')
// 推送GitHub
// push to GitHub
step('\nPushing to GitHub...')
3.3.2 获取targetVersion并校验
let targetVersion = args._[0]
// 不存在当前版本
if (!targetVersion) {
// no explicit version, offer suggestions
const { release } = await prompt({
type: 'select',
name: 'release',
message: 'Select release type',
// 生成选项,对于不同的版本
choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
})
// 如果选择自定义版本,手动输入
if (release === 'custom') {
targetVersion = (
await prompt({
type: 'input',
name: 'version',
message: 'Input custom version',
initial: currentVersion
})
).version
} else {
// 如果是选择非自定 patch (3.2.2),获取匹配中()的内容
targetVersion = release.match(/\((.*)\)/)[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
}
3.3.3 jest 是否运行测试用例
if (!skipTests && !isDryRun) {
// 执行.bin/jest,第二个是参数清除缓存
await run(bin('jest'), ['--clearCache'])
// 运行 pnpm test
await run('pnpm', ['test', '--', '--bail'])
} else {
console.log(`(skipped)`)
}
3.3.4 updateVersions 更新所有包的开发依赖
主要更新vue和@vue/开头的版本
// 所有的版本保持一致性,无论是否变更
function updateVersions(version) {
// 1. update root package.json 更新跟目录的package.json
updatePackage(path.resolve(__dirname, '..'), version)
// 2. update all packages 更新packages下面的包
packages.forEach(p => updatePackage(getPkgRoot(p), version))
}
function updatePackage(pkgRoot, version) {
// 解析包下面的package.json文件
const pkgPath = path.resolve(pkgRoot, 'package.json')
// 解析JSON
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
pkg.version = version
// 更新dependencies
updateDeps(pkg, 'dependencies', version)
// 更新peerDependencies
updateDeps(pkg, 'peerDependencies', version)
// 将新的pkg写入到package.json文件中
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
// 'dependencies', 'peerDependencies'
function updateDeps(pkg, depType, version) {
// 获取deps对象
const deps = pkg[depType]
// 不存在就返回
if (!deps) return
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
}
})
}
peerDependencies的作用:
peerDependencies的目的是提示宿主环境去安装满足插件peerDependencies所指定依赖的包,然后在插件import或者require所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题。
当你写的包a里面依赖另一个包b,而这个包b是引用这个包a的业务的常用的包的时候,建议写在peerDependencies里,避免重复下载/多个版本共存
3.3.5 build打包所有packages
if (!skipBuild && !isDryRun) {
// 运行pnpm run build --release
await run('pnpm', ['run', 'build', '--', '--release'])
// test generated dts files
step('\nVerifying type declarations...')
// pnpm run test-dts-only
await run('pnpm', ['run', 'test-dts-only'])
} else {
console.log(`(skipped)`)
}
3.3.6 生成CHANGELOG
await run(`pnpm`, ['run', 'changelog'])
// shell
conventional-changelog -p angular -i CHANGELOG.md -s
使用了这个包 conventional-changelog-cli,会生成CHANGELOG.md,里面都是commit信息
3.3.7 install 升级pnpm的lock
await run(`pnpm`, ['install', '--prefer-offline'])
安装包的锁,pnpm 默认支持workspace,这里的包管理模式采用的是monorepo,整体一个仓库,所以直接在根目录installl就可以了
—prefer-offline: 表示优先使用离线下载,主要目的,只是为了升级packages的lock记录
3.3.8 commit提交代码
// 运行git diff
const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
// 有值,运行git add,git commit
if (stdout) {
step('\nCommitting changes...')
// 如果没有dry直接运行,这个函数在上面3.2有讲到
await runIfNotDry('git', ['add', '-A'])
await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`])
} else {
console.log('No changes to commit.')
}
3.3.9 publishPackage发布包
// packages是目录数组
for (const pkg of packages) {
await publishPackage(pkg, targetVersion, runIfNotDry)
}
// 包名, 版本, runIfNotDry函数
async function publishPackage(pkgName, version, runIfNotDry) {
// 跳过的包列表,目前是空的,可能为了以后不再
if (skippedPackages.includes(pkgName)) {
return
}
// 获取包路径
const pkgRoot = getPkgRoot(pkgName)
// 解析包的package.json
const pkgPath = path.resolve(pkgRoot, 'package.json')
// JSON转对象
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
// 私有包不需要发布,目前有4个包runtime-test,sfc-playground,size-check,template-explorer
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.
// 版本的tag
let releaseTag = null
if (args.tag) {
releaseTag = args.tag
} else if (version.includes('alpha')) {
releaseTag = 'alpha'
} else if (version.includes('beta')) {
releaseTag = 'beta'
} else if (version.includes('rc')) {
releaseTag = 'rc'
// 针对vue包,tag是next,目前还不是稳定版,当3.x变成default时,会删除next
} else if (pkgName === 'vue') {
// TODO remove when 3.x becomes default
releaseTag = 'next'
}
// TODO use inferred release channel after official 3.0 release
// const releaseTag = semver.prerelease(version)[0] || null
step(`Publishing ${pkgName}...`)
try {
await runIfNotDry(
// note: use of yarn is intentional here as we rely on its publishing
// behavior.
'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
}
}
}
else if (pkgName === 'vue') {
// TODO remove when 3.x becomes default
releaseTag = 'next'
}
上面的代码说明了为什么安装vue3.0还是使用的vue@next,发布到npm还是用的next这个tag
3.3.10 发布到github
// 打tag
await runIfNotDry('git', ['tag', `v${targetVersion}`])
// 提交tag到远端
await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
// 提交commit到远端
await runIfNotDry('git', ['push'])
3.3.11 结尾部分
// 如果是空运行,打印空运行的结尾
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- '
)}`
)
)
}
4 总结
vue-next的构建代码,完全可以用在公司项目中,避免了多项目的发版问题,不过感觉只适合monorepo模式,不太适合multirepo.可以研究一下.
monorepo和multirepo的区别,一个是单仓库,一个是多仓库,两者都是管理组织代码的方式