源码学习目录

1. 前言

1.1 学习内容

本文涉及vue-next/scripts/release.js文件,

1.2 环境

  1. 操作系统: macOS 12.0.1
  2. 浏览器: Chrome 94.0.4606.81
  3. shell: zsh
  4. node 16.13.0
  5. vue 3.2.21
  6. rollup 2.38.5

    1.3 get知识点

  7. 熟悉 vuejs 发布流程

  8. 学会调试 nodejs 代码
  9. 动手优化公司项目发布流程

    1.4 发布流程

    第三期 | vue 3.2 是怎么发布的 release - 图1

    2 环境准备

    打开项目,可以通过README.md和CHANGELOG.md看到项目相关信息

    2.1 严格校验使用yarn安装依赖

    看看package.json文件
    1. "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) }
  1. vue-next 3.2.4的版本判断的包工具是yarn,现在切换到了pnpm,node版本也切换到了16<br />其他相关[npm钩子](https://docs.npmjs.com/cli/v6/using-npm/scripts)
  2. <a name="lLi8y"></a>
  3. ## 2.2 调试 vue-next/scripts/release.js 文件
  4. package.json文件,在脚本的后面加上了--dry,
  5. ```javascript
  6. // --dry 不执行测试和编译 、不执行 推送git等操作
  7. // 只是打印,后文再详细讲述
  8. "release": "node scripts/release.js --dry"

vscode点击dubug就可以开始调试了,命令行会出现debugger attached字段,左侧call Stack会出现node进程和release.js文件,这就开启了正常的调试
image.png

3 开始

3.1 release.js引入的包

通过调试,运行命令 node scripts/release.js --dry

  1. // minimist获取命令行参数
  2. const args = require('minimist')(process.argv.slice(2)) // {_: Array(0), dry: true}
  3. // process.argv.slice(2)截取从下标2开始的数组,因为前面两个值是node的绝对路径和release.js文件的绝对路径,影响运行
  4. const fs = require('fs')
  5. const path = require('path')
  6. // 命令行console添加颜色
  7. const chalk = require('chalk')
  8. // 版本管理
  9. const semver = require('semver')
  10. // 获取当前版本
  11. const currentVersion = require('../package.json').version
  12. // 交互式询问 CLI, prompt异步函数
  13. const { prompt } = require('enquirer')
  14. // 执行脚本
  15. const execa = require('execa')

3.1.1 minimist 获取命令行参数

npm包 minimist

  1. // 使用说明
  2. $ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
  3. { _: [ 'foo', 'bar', 'baz' ],
  4. x: 3,
  5. y: 4,
  6. n: 5,
  7. a: true,
  8. b: true,
  9. c: true,
  10. beep: 'boop' }

目前包已经两年没发新版本了,40+issue

3.1.2 chalk命令行log添加颜色

chalk

  1. chalk.red('Hello', chalk.underline.bgBlue('world') + '!')

3.1.3 semver 包的版本管理

semver

  1. const semver = require('semver')
  2. semver.valid('1.2.3') // '1.2.3'
  3. semver.valid('a.b.c') // null
  4. semver.clean(' =v1.2.3 ') // '1.2.3'
  5. semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true
  6. semver.gt('1.2.3', '9.8.7') // false
  7. semver.lt('1.2.3', '9.8.7') // true
  8. semver.minVersion('>=1.0.0') // '1.0.0'
  9. semver.valid(semver.coerce('v2')) // '2.0.0'
  10. semver.valid(semver.coerce('42.6.7.9.3-alpha')) // '42.6.7'

3.1.4 enquirer 交互式询问 CLI

enquirer

  1. // 支持选择,输入,确定等cli交互,非常方便
  2. const { prompt } = require('enquirer')
  3. const response = await prompt([
  4. {
  5. type: 'input',
  6. name: 'name',
  7. message: 'What is your name?'
  8. },
  9. {
  10. type: 'input',
  11. name: 'username',
  12. message: 'What is your username?'
  13. }
  14. ]);
  15. console.log(response); // { name: 'Edward Chan', username: 'edwardmchan' }

3.1.5 execa 执行命令

execa

  1. import {execa} from 'execa';
  2. const {stdout} = await execa('echo', ['unicorns']);
  3. console.log(stdout);
  4. //=> 'unicorns'

3.2 release.js前置配置参数和方法

  1. // 上一次预发布变量,自定义参数,或者package.version的版本,例如 3.22.4-beta => beta,没有返回null
  2. const preId =
  3. args.preid ||
  4. (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
  5. // 干运行,不真正很执行代码
  6. const isDryRun = args.dry
  7. // 跳过测试
  8. const skipTests = args.skipTests
  9. // 跳过打包
  10. const skipBuild = args.skipBuild
  11. // 拿到packages下面的文件夹,过滤非ts
  12. const packages = fs
  13. .readdirSync(path.resolve(__dirname, '../packages'))
  14. .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
  15. // 跳过packages
  16. const skippedPackages = []
  17. // 版本递增
  18. const versionIncrements = [
  19. // 补丁
  20. 'patch',
  21. // 小版本
  22. 'minor',
  23. // 重大版本
  24. 'major',
  25. // 如果是预发布环境,增加预发布的版本
  26. ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
  27. ]
  28. // 生成递增版本, (当前版本,版本key(patch等),'beta')
  29. const inc = i => semver.inc(currentVersion, i, preId)
  30. // 获取node_modules/.bin下面的脚本
  31. const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
  32. // 执行bin脚本
  33. const run = (bin, args, opts = {}) =>
  34. execa(bin, args, { stdio: 'inherit', ...opts })
  35. // 空运行,只打印
  36. const dryRun = (bin, args, opts = {}) =>
  37. console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
  38. // 相当于包装了一层run,判断执行哪个run函数,空运行还是真实运行
  39. const runIfNotDry = isDryRun ? dryRun : run
  40. // 获取packages跟路径
  41. const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
  42. // 打印下一步
  43. const step = msg => console.log(chalk.cyan(msg))

3.3 main()函数

3.3.1 主流程

  1. // 发布前运行测试
  2. // run tests before release
  3. step('\nRunning tests...')
  4. // 更新所有包的依赖版本
  5. // update all package versions and inter-dependencies
  6. step('\nUpdating cross dependencies...')
  7. // 打包所有包的类型
  8. // build all packages with types
  9. step('\nBuilding all packages...')
  10. // 输出测试文件
  11. // test generated dts files
  12. step('\nVerifying type declarations...')
  13. // 生成变更日志
  14. // generate changelog
  15. step('\nGenerating changelog...')
  16. // 更新pnpm锁
  17. // update pnpm-lock.yaml
  18. step('\nUpdating lockfile...')
  19. // 如果有git diff,
  20. const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
  21. if (stdout)
  22. step('\nCommitting changes...')
  23. // 发布包
  24. // publish packages
  25. step('\nPublishing packages...')
  26. // 推送GitHub
  27. // push to GitHub
  28. step('\nPushing to GitHub...')

3.3.2 获取targetVersion并校验

  1. let targetVersion = args._[0]
  2. // 不存在当前版本
  3. if (!targetVersion) {
  4. // no explicit version, offer suggestions
  5. const { release } = await prompt({
  6. type: 'select',
  7. name: 'release',
  8. message: 'Select release type',
  9. // 生成选项,对于不同的版本
  10. choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
  11. })
  12. // 如果选择自定义版本,手动输入
  13. if (release === 'custom') {
  14. targetVersion = (
  15. await prompt({
  16. type: 'input',
  17. name: 'version',
  18. message: 'Input custom version',
  19. initial: currentVersion
  20. })
  21. ).version
  22. } else {
  23. // 如果是选择非自定 patch (3.2.2),获取匹配中()的内容
  24. targetVersion = release.match(/\((.*)\)/)[1]
  25. }
  26. }
  27. if (!semver.valid(targetVersion)) {
  28. throw new Error(`invalid target version: ${targetVersion}`)
  29. }
  30. // 是否确定要发布
  31. const { yes } = await prompt({
  32. type: 'confirm',
  33. name: 'yes',
  34. message: `Releasing v${targetVersion}. Confirm?`
  35. })
  36. if (!yes) {
  37. return
  38. }

3.3.3 jest 是否运行测试用例

  1. if (!skipTests && !isDryRun) {
  2. // 执行.bin/jest,第二个是参数清除缓存
  3. await run(bin('jest'), ['--clearCache'])
  4. // 运行 pnpm test
  5. await run('pnpm', ['test', '--', '--bail'])
  6. } else {
  7. console.log(`(skipped)`)
  8. }

3.3.4 updateVersions 更新所有包的开发依赖

主要更新vue和@vue/开头的版本
image.png

  1. // 所有的版本保持一致性,无论是否变更
  2. function updateVersions(version) {
  3. // 1. update root package.json 更新跟目录的package.json
  4. updatePackage(path.resolve(__dirname, '..'), version)
  5. // 2. update all packages 更新packages下面的包
  6. packages.forEach(p => updatePackage(getPkgRoot(p), version))
  7. }
  8. function updatePackage(pkgRoot, version) {
  9. // 解析包下面的package.json文件
  10. const pkgPath = path.resolve(pkgRoot, 'package.json')
  11. // 解析JSON
  12. const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
  13. pkg.version = version
  14. // 更新dependencies
  15. updateDeps(pkg, 'dependencies', version)
  16. // 更新peerDependencies
  17. updateDeps(pkg, 'peerDependencies', version)
  18. // 将新的pkg写入到package.json文件中
  19. fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
  20. }
  21. // 'dependencies', 'peerDependencies'
  22. function updateDeps(pkg, depType, version) {
  23. // 获取deps对象
  24. const deps = pkg[depType]
  25. // 不存在就返回
  26. if (!deps) return
  27. Object.keys(deps).forEach(dep => {
  28. if (
  29. dep === 'vue' ||
  30. (dep.startsWith('@vue') && packages.includes(dep.replace(/^@vue\//, '')))
  31. ) {
  32. console.log(
  33. chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`)
  34. )
  35. deps[dep] = version
  36. }
  37. })
  38. }

peerDependencies的作用:

peerDependencies的目的是提示宿主环境去安装满足插件peerDependencies所指定依赖的包,然后在插件import或者require所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题。

当你写的包a里面依赖另一个包b,而这个包b是引用这个包a的业务的常用的包的时候,建议写在peerDependencies里,避免重复下载/多个版本共存

3.3.5 build打包所有packages

  1. if (!skipBuild && !isDryRun) {
  2. // 运行pnpm run build --release
  3. await run('pnpm', ['run', 'build', '--', '--release'])
  4. // test generated dts files
  5. step('\nVerifying type declarations...')
  6. // pnpm run test-dts-only
  7. await run('pnpm', ['run', 'test-dts-only'])
  8. } else {
  9. console.log(`(skipped)`)
  10. }

3.3.6 生成CHANGELOG

  1. await run(`pnpm`, ['run', 'changelog'])
  2. // shell
  3. conventional-changelog -p angular -i CHANGELOG.md -s

使用了这个包 conventional-changelog-cli,会生成CHANGELOG.md,里面都是commit信息

3.3.7 install 升级pnpm的lock

  1. await run(`pnpm`, ['install', '--prefer-offline'])

安装包的锁,pnpm 默认支持workspace,这里的包管理模式采用的是monorepo,整体一个仓库,所以直接在根目录installl就可以了
—prefer-offline: 表示优先使用离线下载,主要目的,只是为了升级packages的lock记录

3.3.8 commit提交代码

  1. // 运行git diff
  2. const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
  3. // 有值,运行git add,git commit
  4. if (stdout) {
  5. step('\nCommitting changes...')
  6. // 如果没有dry直接运行,这个函数在上面3.2有讲到
  7. await runIfNotDry('git', ['add', '-A'])
  8. await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`])
  9. } else {
  10. console.log('No changes to commit.')
  11. }

3.3.9 publishPackage发布包

  1. // packages是目录数组
  2. for (const pkg of packages) {
  3. await publishPackage(pkg, targetVersion, runIfNotDry)
  4. }
  5. // 包名, 版本, runIfNotDry函数
  6. async function publishPackage(pkgName, version, runIfNotDry) {
  7. // 跳过的包列表,目前是空的,可能为了以后不再
  8. if (skippedPackages.includes(pkgName)) {
  9. return
  10. }
  11. // 获取包路径
  12. const pkgRoot = getPkgRoot(pkgName)
  13. // 解析包的package.json
  14. const pkgPath = path.resolve(pkgRoot, 'package.json')
  15. // JSON转对象
  16. const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
  17. // 私有包不需要发布,目前有4个包runtime-test,sfc-playground,size-check,template-explorer
  18. if (pkg.private) {
  19. return
  20. }
  21. // For now, all 3.x packages except "vue" can be published as
  22. // `latest`, whereas "vue" will be published under the "next" tag.
  23. // 版本的tag
  24. let releaseTag = null
  25. if (args.tag) {
  26. releaseTag = args.tag
  27. } else if (version.includes('alpha')) {
  28. releaseTag = 'alpha'
  29. } else if (version.includes('beta')) {
  30. releaseTag = 'beta'
  31. } else if (version.includes('rc')) {
  32. releaseTag = 'rc'
  33. // 针对vue包,tag是next,目前还不是稳定版,当3.x变成default时,会删除next
  34. } else if (pkgName === 'vue') {
  35. // TODO remove when 3.x becomes default
  36. releaseTag = 'next'
  37. }
  38. // TODO use inferred release channel after official 3.0 release
  39. // const releaseTag = semver.prerelease(version)[0] || null
  40. step(`Publishing ${pkgName}...`)
  41. try {
  42. await runIfNotDry(
  43. // note: use of yarn is intentional here as we rely on its publishing
  44. // behavior.
  45. 'yarn',
  46. [
  47. 'publish',
  48. '--new-version',
  49. version,
  50. ...(releaseTag ? ['--tag', releaseTag] : []),
  51. '--access',
  52. 'public'
  53. ],
  54. {
  55. cwd: pkgRoot,
  56. stdio: 'pipe'
  57. }
  58. )
  59. console.log(chalk.green(`Successfully published ${pkgName}@${version}`))
  60. } catch (e) {
  61. if (e.stderr.match(/previously published/)) {
  62. console.log(chalk.red(`Skipping already published: ${pkgName}`))
  63. } else {
  64. throw e
  65. }
  66. }
  67. }
  1. else if (pkgName === 'vue') {
  2. // TODO remove when 3.x becomes default
  3. releaseTag = 'next'
  4. }

上面的代码说明了为什么安装vue3.0还是使用的vue@next,发布到npm还是用的next这个tag

3.3.10 发布到github

  1. // 打tag
  2. await runIfNotDry('git', ['tag', `v${targetVersion}`])
  3. // 提交tag到远端
  4. await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
  5. // 提交commit到远端
  6. await runIfNotDry('git', ['push'])

3.3.11 结尾部分

  1. // 如果是空运行,打印空运行的结尾
  2. if (isDryRun) {
  3. console.log(`\nDry run finished - run git diff to see package changes.`)
  4. }
  5. // 如果有过滤的包,打印出来
  6. if (skippedPackages.length) {
  7. console.log(
  8. chalk.yellow(
  9. `The following packages are skipped and NOT published:\n- ${skippedPackages.join(
  10. '\n- '
  11. )}`
  12. )
  13. )
  14. }

4 总结

vue-next的构建代码,完全可以用在公司项目中,避免了多项目的发版问题,不过感觉只适合monorepo模式,不太适合multirepo.可以研究一下.
monorepo和multirepo的区别,一个是单仓库,一个是多仓库,两者都是管理组织代码的方式

参考

  1. Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?
  2. REPO 风格之争:MONO VS MULTI