vue3.2的发布的release.js源码

1,386 阅读5分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

之所以会学习源码,主要是对他能运行这些命令的好奇。这篇源码是尤雨溪自己写的,主要的作用是发布vue。如果是我的话,我会用lerna来做管理,但是尤大是不会用这个的。

const args = require('minimist')(process.argv.slice(2))
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')
  1. 我们看到的是引入了minimist,这个是一个轻量级的命令行参数解析引擎。process.argv.slice(2)这个是node提供的原生的一个API,argv返回的是一个数组,数组的第一项是你node的安装文件夹,第二项是你的执行的文件的命令。他这句话的意思是截取从第二个开始直到结束你输入的命令。
  2. fs是一个node提供读取系统文件的API。
  3. path是node提供的解析路径的一个API。
  4. chalk 是一个给cmd文字赋上颜色的一个库。
  5. semver 用于版本的比较,提供了一系列的API给我们调用,提高开发的效率。
  6. currentVersion 从package.json里面读取当前的版本信息。
  7. enquirer是交互式询问用户输入,大家可能还记得js原生的API提供了prompt这样一个方法。
  8. execa执行你在终端输入的命令的一个库。

上面一共引入了8个内库,有node提供的内置模块,也有外接模块。来共同实现效果。


const preId =
  args.preid ||
  (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])

args.preid的意思是说,当你输入yarn run release --preid=beta 的时候他就能获取到值。说明了args是一个对象。如果你没有这样输入,那么就取currentVersion,在currentVersion外面有一个semver.prerelease方法,这个方法主要是用来返回一个预发布组件的数组。如果存在就取第零项。

 const skipTests = args.skipTests
 const skipBuild = args.skipBuild

根据命名可以推测出,是跳过测试的。yarn run release --skipTests 他就会跳过对应的测试。

根据命名可以推测出,是跳过打包的。yarn run release --skipBuild 他就会跳过打包的过程。

const packages = fs
  .readdirSync(path.resolve(__dirname, '../packages'))
  .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))

这个是同步读取了packages文件里面的文件,然后过滤掉不是以ts结尾和不是以.开头的文件。

const skippedPackages = []

这个空的数组指的是跳过的包。

const versionIncrements = [
  'patch',
  'minor',
  'major',
  ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]

这个是一个版本增加的数组。patch是补丁。Major 是主要的版本。minor 修正的版本号。然后他根据preId(之前定义过的) 来判断是一个数组还是一个空数组,然后把数组用延展符展开。

const inc = i => semver.inc(currentVersion, i, preId)

这个是一个箭头函数,调用了 semver.inc方法,传入了当前版本,i值,和preId。

首先,我们要明白semver.inc的用法。

semver.inc('1.2.3', 'prerelease', 'beta') // '1.2.4-beta.0'
const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)

这个函数的主要作用是得到一个完成的路径。

const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })
// 跑在终端的命令   bin 是面定义了的问价的路径  execa主要是接收三个参数(文件,参数,选项) 是一个混合的方法。
const dryRun = (bin, args, opts = {}) =>
  console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)

这个是打印一句话。

const runIfNotDry = isDryRun ? dryRun : run  //const isDryRun = args.dry 这个对应这个,如果args.dry有值的情况下,就是dryRun(打印的函数),不然就是run(执行终端的函数)。
//获取文件的根文件
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
//用chalk打印一句话
const step = msg => console.log(chalk.cyan(msg)) 

下面是main函数的部分。

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

看到main函数这样调用,我们知道他是一个promise对象。那么怎么写这个main函数呢?

async function main() {
    //获取当前输入的版本,是一个对象。为什么这里是一个对象,是因为当你输入yarn run release 3.2.5的时候,他打印的是这个{ _: [ '3.2.3' ] }   对象此时没有键名,只有键值。
    let targetVersion = args._[0]
    if (!targetVersion) { // 如果targetVersion不存在,就调用交互的命令
        const { release } = await prompt({
        type: 'select',
        name: 'release',
        message: 'Select release type',
        choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
    })
        if (release === 'custom') {  //如果调用了交互式命令,选择的是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}`)
}
    // 确认要 release
const { yes } = await prompt({
    type: 'confirm',
    name: 'yes',
    message: `Releasing v${targetVersion}. Confirm?`
})
    // false 直接返回
if (!yes) {
   return
}
​
//执行测试用例  如果既没有跳过测试,也没有输入args.dry
step('\nRunning tests...')
if (!skipTests && !isDryRun) {
    await run(bin('jest'), ['--clearCache'])
    await run('yarn', ['test', '--bail'])
} else {
    console.log(`(skipped)`)
}
// 更新包的过程
step('\nUpdating cross dependencies...')
updateVersions(targetVersion)
​
 // 自动生成包的types文件
step('\nBuilding all packages...')
  if (!skipBuild && !isDryRun) {
    await run('yarn', ['build', '--release'])
    // test generated dts files
    step('\nVerifying type declarations...')
    await run('yarn', ['test-dts-only'])
  } else {
    console.log(`(skipped)`)
  }
 //打印改变的日志
 await run(`yarn`, ['changelog'])
​
​
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)
  }
​
  // push to GitHub
  step('\nPushing to GitHub...')
    // 打个tag
  await runIfNotDry('git', ['tag', `v${targetVersion}`])
    //推送tag
  await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
    // push 到远程
  await runIfNotDry('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- '
        )}`
      )
    )
  }
}
​
//更新所有包的版本号和内部vue相关的依赖版本号
function updateVersions(version) {
  // 1. update root package.json
  updatePackage(path.resolve(__dirname, '..'), version)
  // 2. update all packages
  packages.forEach(p => updatePackage(getPkgRoot(p), version))
}
​
// 更新包的版本号  修改dependencies,peerDependencies中的依赖
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')
}
​
​
//用循环不断的更新包的version版本。判断了以@vue开头的。
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
  }
​
  // For now, all 3.x packages except "vue" can be published as
  // `latest`, whereas "vue" will be published under the "next" 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'
  }
​
  // 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
    }
  }
}
​
​

这是源码共读的第xx期,链接:juejin.cn/post/708498…