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

26 阅读5分钟

1、严格校验使用 yarn 安装依赖

{
  "private": true,
    "version": "3.2.4",
      "workspaces": [
        "packages/*"
      ],
        "scripts": {
          // --dry 参数 调试 代码也建议加
          // 不执行测试和编译 、不执行 推送git等操作
          // 也就是说空跑,只是打印,后文再详细讲述
          "release": "node scripts/release.js --dry",
            "preinstall": "node ./scripts/checkYarn.js",  //安装依赖包
        }
}
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)
} 
//必须使用pnmp,否则会警告

如果你想忽略这个前置的钩子判断,可以使用 --ignore-scripts 命令

2、调试 vue-next/scripts/release.js 文件

点击package.json中script上方的调试,选择release,界面弹出

1、第一部分

//命令行参数解析
const args = require('minimist')(process.argv.slice(2))
// args { _: [], dry: true }
//文件模块
const fs = require('fs')
//路径模块
const path = require('path')
// 控制台 多色显示
const chalk = require('chalk')
//semver 语义化版本
const semver = require('semver')
//当前版本
const currentVersion = require('../package.json').version
//交互式询问用户输入
const { prompt } = require('enquirer')
// 执行子进程命令   简单说 就是在终端命令行执行 命令
const execa = require('execa')

1、minimist 命令行参数解析

const args = require('minimist')(process.argv.slice(2))
package.json:命令行:node scripts/release.js --dry
打印:args { _: [], dry: true }
$ node example/parse.js -a beep -b boop
{ _: [], a: 'beep', b: 'boop' }

$ 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' }

2、第二部分

//对应 pnpm run release --preid=beta
const preId =
      args.preid ||
      (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
//对应 pnpm run release --dry
const isDryRun = args.dry
// 对应 pnpm run release --skipTests
const skipTests = args.skipTests
//对应 pnpm run release --skipBuild
const skipBuild = args.skipBuild
// 读取 packages 文件夹,过滤掉 不是 .ts文件 结尾 并且不是 . 开头的文件夹
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'] : [])
]

3、第三部分

//版本号
const inc = i => semver.inc(currentVersion, i, preId)

const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 获取 bin 命令
//bin('jest') 相当于在命令终端,项目根目录 运行 ./node_modules/.bin/jest 命令。
//run 真实在终端跑命令
const run = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
//dryRun 不运行 打印
const dryRun = (bin, args, opts = {}) =>
console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
//isDryRun 关键:决定是否在控制台上运行!!
const runIfNotDry = isDryRun ? dryRun : run
// 获取包的路径
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
// 控制台输出
const step = msg => console.log(chalk.cyan(msg))

1、bin 函数

bin('jest')

//相当于在命令终端,项目根目录 运行 ./node_modules/.bin/jest 命令。

4、第四部分 主流程

//主流程
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('pnpm', ['test', '--', '--bail'])
  } else {
    console.log(`(skipped)`)
  }
  
  // update all package versions and inter-dependencies
  //更新所有包的版本号和内部 vue 相关依赖版本号
  step('\nUpdating cross dependencies...')
  updateVersions(targetVersion)
  
  // build all packages with types
  //打包编译所有包
  step('\nBuilding all packages...')
  if (!skipBuild && !isDryRun) {
    await run('pnpm', ['run', 'build', '--', '--release'])
    // test generated dts files
    step('\nVerifying type declarations...')
    await run('pnpm', ['run', 'test-dts-only'])
  } else {
    console.log(`(skipped)`)
  }
  
  // generate changelog
  //生成 changelog
  step('\nGenerating changelog...')
  await run(`pnpm`, ['run', 'changelog'])
  
  // update pnpm-lock.yaml
  //更新 changelog
  step('\nUpdating lockfile...')
  await run(`pnpm`, ['install', '--prefer-offline'])
  
  //经过更新版本号后,有文件改动,于是git diff。 是否有文件改动,如果有提交
  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
  //提交到github
  step('\nPushing to GitHub...')
  await runIfNotDry('git', ['tag', `v${targetVersion}`])
  await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
  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- '
        )}`
      )
    )
  }
  console.log()
}

1、选择发布的版本

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}`)
}
  

如果是custom,则输入自定义版本号

2、是否安装该版本(按键选择)

const { yes } = await prompt({
  type: 'confirm',
  name: 'yes',
  message: `Releasing v${targetVersion}. Confirm?`
})

if (!yes) {
  return
  }

3、执行测试用例

// run tests before release
//执行测试用例
step('\nRunning tests...')
if (!skipTests && !isDryRun) {
  await run(bin('jest'), ['--clearCache'])
  await run('pnpm', ['test', '--', '--bail'])
} else {
  console.log(`(skipped)`)
  }

如果skipTests与isDryRun存在,则执行jest命令

4、更新所有包的版本号和内部 vue 相关依赖版本号

// update all package versions and inter-dependencies
//更新所有包的版本号和内部 vue 相关依赖版本号
step('\nUpdating cross dependencies...')
updateVersions(targetVersion)
}

function updateVersions(version) {
  // 1. update root package.json
  updatePackage(path.resolve(__dirname, '..'), version)
  // 2. update all packages
  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
    }
  })
}

5、打包编译所有包

//打包编译所有包
step('\nBuilding all packages...')
if (!skipBuild && !isDryRun) {
  await run('pnpm', ['run', 'build', '--', '--release'])
  // test generated dts files
  step('\nVerifying type declarations...')
  await run('pnpm', ['run', 'test-dts-only'])
} else {
  console.log(`(skipped)`)
  }

如果skipBuild与isDryRun存在,则执行build命令

6、生成日志

//生成 changelog
step('\nGenerating changelog...')
  await run(`pnpm`, ['run', 'changelog']) //执行changelog命令

7、更新pnpm-lock.yaml,安装依赖包

// update pnpm-lock.yaml
//更新 pnpm-lock.yaml
step('\nUpdating lockfile...')
  await run(`pnpm`, ['install', '--prefer-offline'])

8、提交代码

//经过更新版本号后,有文件改动,于是git diff。 是否有文件改动,如果有提交
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.')
}

10、发布包

//发布包
step('\nPublishing packages...')
for (const pkg of packages) {
  await publishPackage(pkg, targetVersion, runIfNotDry)
}

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
  }
  
  //发布包的版本
  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'
  }
  
  // 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
    }
  }
}

11、提交到github

// push to GitHub
//提交到github
step('\nPushing to GitHub...')
await runIfNotDry('git', ['tag', `v${targetVersion}`])
await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
await runIfNotDry('git', ['push'])

if (isDryRun) {
  console.log(`\nDry run finished - run git diff to see package changes.`)
  }

12、检查是否有跳过文件

if (skippedPackages.length) {
  console.log(
    chalk.yellow(
      `The following packages are skipped and NOT published:\n- ${skippedPackages.join(
        '\n- '
      )}`
    )
  )
}
  console.log()

3、流程

注:小程序打包构建

基于 CI 实现微信小程序的持续构建

小打卡小程序自动化构建及发布的工程化实践

4、感谢

这一期开始第一部分很多很多看不懂,决定很沮丧来着,但是慢慢看后面的代码,慢慢调试,突然明朗了,也理解了前面开始看不懂的地方。不要为了源码而源码,源码终究是为了实现服务的。冲,今天学的很开心。