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') // nullsemver.clean(' =v1.2.3 ') // '1.2.3'semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // truesemver.gt('1.2.3', '9.8.7') // falsesemver.lt('1.2.3', '9.8.7') // truesemver.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,没有返回nullconst preId =args.preid ||(semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])// 干运行,不真正很执行代码const isDryRun = args.dry// 跳过测试const skipTests = args.skipTests// 跳过打包const skipBuild = args.skipBuild// 拿到packages下面的文件夹,过滤非tsconst packages = fs.readdirSync(path.resolve(__dirname, '../packages')).filter(p => !p.endsWith('.ts') && !p.startsWith('.'))// 跳过packagesconst 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 releasestep('\nRunning tests...')// 更新所有包的依赖版本// update all package versions and inter-dependenciesstep('\nUpdating cross dependencies...')// 打包所有包的类型// build all packages with typesstep('\nBuilding all packages...')// 输出测试文件// test generated dts filesstep('\nVerifying type declarations...')// 生成变更日志// generate changelogstep('\nGenerating changelog...')// 更新pnpm锁// update pnpm-lock.yamlstep('\nUpdating lockfile...')// 如果有git diff,const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })if (stdout)step('\nCommitting changes...')// 发布包// publish packagesstep('\nPublishing packages...')// 推送GitHub// push to GitHubstep('\nPushing to GitHub...')
3.3.2 获取targetVersion并校验
let targetVersion = args._[0]// 不存在当前版本if (!targetVersion) {// no explicit version, offer suggestionsconst { 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 testawait run('pnpm', ['test', '--', '--bail'])} else {console.log(`(skipped)`)}
3.3.4 updateVersions 更新所有包的开发依赖
主要更新vue和@vue/开头的版本
// 所有的版本保持一致性,无论是否变更function updateVersions(version) {// 1. update root package.json 更新跟目录的package.jsonupdatePackage(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')// 解析JSONconst pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))pkg.version = version// 更新dependenciesupdateDeps(pkg, 'dependencies', version)// 更新peerDependenciesupdateDeps(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) returnObject.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 --releaseawait run('pnpm', ['run', 'build', '--', '--release'])// test generated dts filesstep('\nVerifying type declarations...')// pnpm run test-dts-onlyawait run('pnpm', ['run', 'test-dts-only'])} else {console.log(`(skipped)`)}
3.3.6 生成CHANGELOG
await run(`pnpm`, ['run', 'changelog'])// shellconventional-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 diffconst { stdout } = await run('git', ['diff'], { stdio: 'pipe' })// 有值,运行git add,git commitif (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.jsonconst pkgPath = path.resolve(pkgRoot, 'package.json')// JSON转对象const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))// 私有包不需要发布,目前有4个包runtime-test,sfc-playground,size-check,template-explorerif (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.// 版本的taglet releaseTag = nullif (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 defaultreleaseTag = 'next'}// TODO use inferred release channel after official 3.0 release// const releaseTag = semver.prerelease(version)[0] || nullstep(`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 defaultreleaseTag = 'next'}
上面的代码说明了为什么安装vue3.0还是使用的vue@next,发布到npm还是用的next这个tag
3.3.10 发布到github
// 打tagawait 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的区别,一个是单仓库,一个是多仓库,两者都是管理组织代码的方式
