【若川视野 x 源码共读】第3期 | vue 3.2 是怎么发布的 vue-release

208 阅读4分钟

发布的流程

vue-next发布流程.drawio.png

发布要用到的release.js,注释和理解如下

const args = require('minimist')(process.argv.slice(2)) //获取命令行参数
const fs = require('fs') //nodejs处理文件模块
const path = require('path') //nodejs如今模块
const chalk = require('chalk') //给命令行打印,添加样式
const semver = require('semver') //npm的语义化版本解析器
const currentVersion = require('../package.json').version //获取版本号
const { prompt } = require('enquirer') //命令行操作引导
const execa = require('execa') //更加人性化的进程执行

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结尾和以.开始的的文件
// endswidth判断当前字符串是否以给定子串开头,startswith判断当前字符串是否以给定子串结尾,是则返回true
// 否则返回false,ie不支持,需要添加polyfill
const packages = fs
  .readdirSync(path.resolve(__dirname, '../packages'))
  .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
// 需要跳过的包
const skippedPackages = []
//版本递增设置
const versionIncrements = [
  'patch',
  'minor',
  'major',
  ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]
// 帮助函数
const inc = i => semver.inc(currentVersion, i, preId) //生成发布版本号函数
const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name) //执行bin目录下name所代表的的脚本
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)//空跑并打印
const runIfNotDry = isDryRun ? dryRun : run //根据isDryRun真假来赋值
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg) //获取pkg值所对应的绝对路径
const step = msg => console.log(chalk.cyan(msg)) //打印相应的步骤
// 主函数
async function main() {
  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 {
      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
  }
  //在发布前进行测试,空跑跳过测试
  // run tests before release
  step('\nRunning tests...')
  //是否跳过测试&&是否空跑
  if (!skipTests && !isDryRun) {
    await run(bin('jest'), ['--clearCache'])
    await run('yarn', ['test', '--bail'])
  } else {
    console.log(`(skipped)`)
  }
  // 更新所有的包版本和内部依赖
  // update all package versions and inter-dependencies
  step('\nUpdating cross dependencies...')
  updateVersions(targetVersion)
  //构建,如果是空跑的话,则跳过构建
  // build all packages with types
  step('\nBuilding all packages...')
  if (!skipBuild && !isDryRun) {
    await run('yarn', ['build', '--release'])
    // test generated dts files//测试生成的dts文件
    step('\nVerifying type declarations...')
    await run('yarn', ['test-dts-only'])
  } else {
    console.log(`(skipped)`)
  }
  // 生成changelog
  // generate changelog
  await run(`yarn`, ['changelog'])
  // 查看文件是否修改,有则进行commit,无则打印信息
  const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
  if (stdout) {
    step('\nCommitting changes...')
    await runIfNotDry('git', ['add', '-A'])
    await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`])
  } else {
    console.log('No changes to commit.')
  }
  // 发包
  // publish packages
  step('\nPublishing packages...')
  for (const pkg of packages) {
    await publishPackage(pkg, targetVersion, runIfNotDry)
  }
  // 推送到github
  // push to GitHub
  step('\nPushing to GitHub...')
  await runIfNotDry('git', ['tag', `v${targetVersion}`]) //git tag v4.0.1
  await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`]) // git push origin refs/tags/v4.0.1
  await runIfNotDry('git', ['push']) //git push

  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- '
        )}`
      )
    )
  }
  console.log()
}
// 更新版本号
function updateVersions(version) {
  // 1. update root package.json //更新顶层目录的package.json
  updatePackage(path.resolve(__dirname, '..'), version)
  // 2. update all packages //更新package下所有包的package.json
  packages.forEach(p => updatePackage(getPkgRoot(p), version))
}
// 更新包版本
function updatePackage(pkgRoot, version) {
  const pkgPath = path.resolve(pkgRoot, 'package.json')
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
  pkg.version = version
  updateDeps(pkg, 'dependencies', version) //更新生产依赖
  updateDeps(pkg, 'peerDependencies', version) //更新同伴依赖
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
// 更新依赖版本
function updateDeps(pkg, depType, version) {
  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
    }
  })
}
// 发布包
async function publishPackage(pkgName, version, runIfNotDry) {
  if (skippedPackages.includes(pkgName)) {
    //是否有需要跳过的包,有则终止
    return
  }
  const pkgRoot = getPkgRoot(pkgName)
  const pkgPath = path.resolve(pkgRoot, 'package.json')
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
  if (pkg.private) {
    //是否是私有包,是则终止
    return
  }
  // 根据命令行参数及版本信息确定发布包的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'
  } else if (pkgName === 'vue') {
    // TODO remove when 3.x becomes default
    releaseTag = 'next' //包名为vue时,将包的tag设置为next
  }

  // TODO use inferred release channel after official 3.0 release
  // const releaseTag = semver.prerelease(version)[0] || null

  step(`Publishing ${pkgName}...`)
  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
    }
  }
}

main().catch(err => {
  console.error(err)
})

总结

  1. 可以使用这套代码,根据自己的需求,稍加改造,优化项目的发布流程。
  2. release.js引入的npm包可以加入到日常开发中,规范自己的开发,提升自己的效率。
  3. js string的新方法startsWith和endswidth可以用到项目中。
  4. 能用别人造好的轮子就用,不必重复造轮子。