likes
comments
collection
share

学习下 vue.js 是如何发布版本的?

作者站长头像
站长
· 阅读数 17

本文我们通过阅读 vuejs/core/scripts/release.js 文件,来学习下尤大是怎么发布 vue 版本的吧!

环境准备

  • vscode
  • vue 版本 v3.2.45

克隆代码

vue 源码:git clone git@github.com:vuejs/core.git

切换 tag 到 v3.2.45

安装依赖

这里注意一下!!!安装依赖时如果不是使用 pnpm,会输出警告并结束当前进程;

学习下 vue.js 是如何发布版本的?

原因:package.json 文件的 npm scripts 字段中有条 preinstsall 命令,该条命令会在我们执行 npm install 时,先执行 preinstall 命令

// package.json
{
  "scripts": {
    "preinstall": "node ./scripts/preinstall.js",
  }
}

preinstall.js 内部通过环境变量 process.env.npm_execpath获取当前执行的包管理器绝对路径,判断是否匹配到 pnpm,没有则输出警告并结束当前进程。

学习下 vue.js 是如何发布版本的?

为什么会先执行 preinstall 命令呢,这是 npm scripts 约定的一套命名匹配规则,当我们命令以 prepost 开头都会被匹配执行,pre 开头可以看成是命令的前置钩子,post 开头为后置钩子。 例如我定义了一条命令 myscript,那么 premyscriptpostmyscript 也会被执行,执行顺序:premyscript => myscript => postmyscript

{
  "scripts": {
    "premyscript": "在 myscript 前执行",
    "myscript": "xxx",
    "postmyscript": "在 myscript 后执行"
  }
}

此外,scripts 还有其他内置的生命周期钩子,具体见 scripts 字段说明 npm-scripts

运行调试

在脚本命令 release 上点击调式脚本,调试 release.js 源码。

学习下 vue.js 是如何发布版本的?

release.js 源码

我们看到在最后面执行了 main 函数,该函数就是入口,直接从这开始一步一步看。

学习下 vue.js 是如何发布版本的?

总体流程

main 内部执行流程主要做了以下事情:

  1. 确定和校验要发布的版本号;
  2. 判断是否需要执行测试用例;
  3. 更新所有 vue 相关包的版本;
  4. 打包编译所有包及测试类型声明文件;
  5. 生成 changelog;
  6. 更新 pnpm 的 lockfile;
  7. 提交代码 git add、git commit;
  8. 发布新版本包到 npm 官方源;
  9. 代码 push 到 github 仓库。

接下来我们分步看具体代码:

1. 确定和校验要发布的版本号

确定版本号,这里分为两种情况:

  1. 执行 release 命令时指定了版本号,则赋值给 targetVersion变量,例如:pnpm run release 1.2.3, 则 targetVersion 值为 1.2.3

    学习下 vue.js 是如何发布版本的?
  2. 未指定版本号,则提供选项版本号供选择(选项版本号是在当前版本号 currentVersion 的基础上递增);执行命令时若指定 preid 选项参数, 则会多出一些先行版本号选项(例如pnpm run release --perid=alpha),如下图

    学习下 vue.js 是如何发布版本的?

校验版本号,通过调用 semver.valid() 方法来校验版本号是否符合 semver 规范。

相关代码

// args 包含命令行传入的参数
const args = require('minimist')(process.argv.slice(2))
// 版本号规范工具
const semver = require('semver')
// 获取当前版本号,通过读取 package.json version字段
const currentVersion = require('../package.json').version
// 用来创建命令行提示符
const { prompt } = require('enquirer')
// 获取先行版本号,通过读取命令行参数指定或 package.json version 字段
// 例如执行时指定了 --preid 选项,pnpm run release --preid=beta, 则 preId 为 beta
const preId =
  args.preid ||
  (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
// 版本号类型选项,作为未指定版本号时提供的选项
const versionIncrements = [
  'patch',
  'minor',
  'major',
  ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]
// 递增版本号
const inc = i => semver.inc(currentVersion, i, preId)
​
function main() {
    // 获取命令行版本号参数值,例如执行时指定了版本号参数,node release.js 1.2.3, 则 args 为 [_: [1.2.3]], targetVersion 为 1.2.3
    let targetVersion = args._[0]
  // 如果没有通过命令行参数指定版本,则提供选项选择
    if (!targetVersion) {
        // 提供版本选择
        const { release } = await prompt({
            type: 'select',
            name: 'release',
            message: 'Select release type',
            // 版本号选项
            choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
        })
        // 如果选的是 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?`
    })
    // 选择 n,则不再往后执行
    if (!yes) {
        return
    }
    // ...
}
​

引人的相关包:

  • minimist 解析命令行参数
  • semver 版本号规范工具
  • enquirer 创建友好、简单直观的命令行提示符

2. 判断是否需要执行测试用例

相关代码

const args = require('minimist')(process.argv.slice(2))
// 命令执行工具
const execa = require('execa')
// 是否空跑,可通过命令行选项 --dry 指定,例如 pnpm run release --dry
const isDryRun = args.dry
// 是否跳过执行测试,可通过命令行选项 --skipTests 指定
const skipTests = args.skipTests
const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })
​
function main() {
    step('\nRunning tests...')
    // 命令行选项没有指定 --skipTests 和 --isDryRun 时,则执行测试
    if (!skipTests && !isDryRun) {
        await run(bin('jest'), ['--clearCache'])
        await run('pnpm', ['test', '--bail'])
    } else {
        console.log(`(skipped)`)
    }
}
​

引人的相关包:

  • execa 命令执行工具,使用 node 子进程执行

3. 更新所有 vue 相关包的版本号

例如当前版本为 3.2.45,通过上面第一步确定版本号为 3.2.46,则更新以下 vue 相关包的版本号到 3.2.46

学习下 vue.js 是如何发布版本的?

相关代码

// 给终端字符串设置样式
const chalk = require('chalk')
// 获取根目录 packages 下子目录的绝对路径
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
// 根目录 packages 下的所有子文件夹名
const packages = fs
  .readdirSync(path.resolve(__dirname, '../packages'))
  .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
​
function main() {
    // targetVersion 为第一个步骤获取到的值
    step('\nUpdating cross dependencies...')
    updateVersions(targetVersion)
}
​
function updateVersions(version) {
  // 1. 修改根文件夹下 package.json version 字段版本号
  updatePackage(path.resolve(__dirname, '..'), version)
  // 2. 修改 packages 文件夹下所有包及 vue 相关依赖的版本号
  packages.forEach(p => updatePackage(getPkgRoot(p), version))
}
​
function updatePackage(pkgRoot, version) {
  // package.json 路径
  const pkgPath = path.resolve(pkgRoot, 'package.json')
  // 读取 package.json
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
  // 修改 package.json version 字段
  pkg.version = version
  // 更新 dependencies 字段下依赖包版本
  updateDeps(pkg, 'dependencies', version)
  // 更新 peerDependencies 字段下依赖包版本
  updateDeps(pkg, 'peerDependencies', version)
  // 更新 package.json 文件内容
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
// 更新 vue 相关的依赖包版本(以 @vue 开头且是 packages 文件夹下的包)
function updateDeps(pkg, depType, version) {
  const deps = pkg[depType]
  if (!deps) return
  Object.keys(deps).forEach(dep => {
    // 修改版本号,条件:依赖包的名称如果等于 'vue' 或者 以 @vue 开头且为 packages 文件夹下面的包
    if (
      dep === 'vue' ||
      (dep.startsWith('@vue') && packages.includes(dep.replace(/^@vue\//, '')))
    ) {
      console.log(
        chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`)
      )
      deps[dep] = version
    }
  })
}
​

4. 打包编译所有包及测试类型声明文件

相关代码

const args = require('minimist')(process.argv.slice(2))
// 命令行命令执行工具,启动子进程执行
const execa = require('execa')
const isDryRun = args.dry
// 是否跳过执行测试,可通过命令行参数 --skipTests 指定
const skipTests = args.skipTests
// 是否跳过执行打包,可通过命令行参数 --skipBuild 指定
const skipBuild = args.skipBuild
//
const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })
​
function main() {
    step('\nBuilding all packages...')
    // 命令行参数没有指定 --skipBuild 和 --skipTests 时,执行命令 pnpm build --release 和 pnpm test-dts-only
    if (!skipBuild && !isDryRun) {
        await run('pnpm', ['build', '--release'])
        // test generated dts files
        step('\nVerifying type declarations...')
        await run('pnpm', ['test-dts-only'])
    } else {
        console.log(`(skipped)`)
    }
}
​

5. 生成 changelog

相关代码

const execa = require('execa')
const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })
​
async function main() {
    // 执行命令 pnpm changelog
  await run(`pnpm`, ['changelog'])
}
​

changelog 命令,使用 conventional-changelog-cli 包自动生成 git 提交记录的日志文件 CHANGELOG.md

// 根目录下的 package.json
{
    "scripts": {
        "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    },
}

6. 更新 pnpm 的 lockfile

const execa = require('execa')
const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })
​
async function main() {
  // update pnpm-lock.yaml
  step('\nUpdating lockfile...')
  // 安装依赖,--prefer-offline 选项意思是安装依赖时优先使用本地缓存,若本地缺失数据再去远程拉取
  await run(`pnpm`, ['install', '--prefer-offline'])
}
​

7. 提交代码 git add、git commit

相关代码

const args = require('minimist')(process.argv.slice(2))
const execa = require('execa')
// 可通过 --dry 指定
const isDryRun = args.dry
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

function main() {
    // 通过 git diff 是否有文件改动,若有改动,stdout 为 git diff 输出的改动内容
    const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
    if (stdout) {
        step('\nCommitting changes...')
        // git add -A
        await runIfNotDry('git', ['add', '-A'])
        // git commit -m "release: vxxx"
        await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`])
    } else {
        console.log('No changes to commit.')
    }
}

8. 发布新版本包到 npm 官方源

通过 publish 命令发布到 npm 官方源上,发布时需要指定选项 --access public 声明为公共包,因为 vue 相关的包都是在命名空间 @vue 下,而发布带有命名空间的包 npm 会默认为是要发布私有包,发布私有包需要另外付费的哦!

相关代码

const path = require('path')
// packages 根目录下的所有子文件夹名
const packages = fs
  .readdirSync(path.resolve(__dirname, '../packages'))
  .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
// 获取根目录 packages 下子目录的绝对路径
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)

async function main() {
    step('\nPublishing packages...')
    for (const pkg of packages) {
        await publishPackage(pkg, targetVersion, runIfNotDry)
    }
}

async function publishPackage(pkgName, version, runIfNotDry) {
    // 不需要发布的包直接 return 掉
    if (skippedPackages.includes(pkgName)) {
        return
    }
    // 文件夹绝对路径
    const pkgRoot = getPkgRoot(pkgName)
    // package.json 文件的绝对路径
    const pkgPath = path.resolve(pkgRoot, 'package.json')
    // 读取 packaeg.json 文件内容
    const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
    // 如果 private 字段声明为 true 也就是定义为私有包,则不发布
    if (pkg.private) {
        return
    }
    // 确定包 tag,发布时通过 --tag 指定,不指定默认值为 latest
    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'
    }

    step(`Publishing ${pkgName}...`)
    try {
        // note: use of yarn is intentional here as we rely on its publishing behavior.
        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
        }
    }
}

9. 代码 push 到 github 仓库

相关代码

function main() {
    // push to GitHub
    step('\nPushing to GitHub...')
    // 创建版本标签
    await runIfNotDry('git', ['tag', `v${targetVersion}`])
    // 将新版本标签推送到 github 仓库
    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- '
                )}`
            )
        )
    }
}

总结

通过我们逐行阅读源码,了解 vue 3 发布版本包的总体流程,学以致用,以后我们再开发一些包或工具的时候可以借鉴借鉴。

相关参考

转载自:https://juejin.cn/post/7181042799230320677
评论
请登录