Vite 的发布又改了发布方式 😭😭😭😭😭😭,不过其实也不用太担心,改来改去,发布的核心逻辑都是不变的,大家看的时候就参考参考就行了。 这个故事告诉我们,要等代码稳定了之后再写文章 😭😭😭😭😭😭 该文章讲解的 vite 版本
前言
最近在做 monorepo 的 npm 包发布,参考(照抄)了 Vite 的发布方式。然而当我写完的第二天,Vite 重构了它的发布脚本(2022/2/11)😭😭😭 于是,我又再次的修改了我的项目,并写一篇文章来专门介绍一下。
本文将分两个部分:
- 从用户的角度看 Vite 发布
- Vite 发布的实现方式
从用户的角度看 Vite 发布
Vite 的发布重构之后,用户可以通过一个可视化的界面,对 Vite 进行发布。
下图是 GitHub Action 界面,通过手动触发 GitHub Action
的方式对 vite 及一些官方的 vite 插件进行发布。
值得注意的是,只有项目成员才能看到并执行 release 工作流。如果我们需要体验运行该工作流,需要先 fork vite 仓库,再到 Action 界面执行
其中 package 和 type 是可选的,分别对应要发布的 npm 包及版本号的生成方式(如仅增加 minior 修订版本号)
运行之后,就会自动执行工作流,发布 vite 到 npm。
Vite 发布的实现方式
github 的工作流程配置文件,都存储在仓库的 .github/workflows
下。
我们运行的是 release 工作流程,因此我们需要看 .github/workflows/release.yml
的配置
release.yml
我们将 release.yml 拆成几部分来看:
- 定义用户可以选择的参数
- 运行 job,发布到 npm
定义用户的可选参数
可选参数有:
- 运行发布的分支,默认为 main 分支
- 需要发布的 npm 包,默认是 vite
- 版本号生成方式
name: Release
on:
workflow_dispatch:
inputs:
branch:
description: "branch"
required: true
type: string
default: "main"
package:
description: "package"
required: true
type: choice
options:
- vite
- plugin-legacy
- plugin-vue
- plugin-vue-jsx
- plugin-react
- create-vite
type:
description: "type"
required: true
type: choice
options:
- next
- stable
- minor-beta
- major-beta
- minor
- major
效果如下:
表单项 Use workflow from,并没有在 release.yml 中定义,它是 GitHub 运行工作流程时自带的选项。
它的作用是,读取哪个分支的 release.yml ,因为不同分支的 release.yml 可能是不一样的。
如果选择的分支没有 release.yml 文件,则不会再有这三个选项,且流水线不能运行。
运行 job,发布到 npm
主要有以下几个步骤:
- 使用 Ubuntu 镜像进行构建
- 拉取 git 仓库代码,拉取选中分支
- 使用 node 16
- 安装 pnpm
- pnpm install,如果有缓存,则利用缓存进行加速安装
- 在需要发布的包的项目,执行
pnpm run release
jobs:
release:
# prevents this action from running on forks
# 避免 fork 的仓库运行该工作流
if: github.repository == 'vitejs/vite'
name: Release
runs-on: ${{ matrix.os }}
environment: Release
strategy:
matrix:
# pseudo-matrix for convenience, NEVER use more than a single combination
# 伪矩阵,为了方便(可能是 vite 的开发者从别处拷贝的代码做了少量改动),不要使用一个以上的组合
# 意思是使用最新的 ubuntu 系统以及使用 node 16 运行 job
node: [16]
os: [ubuntu-latest]
steps:
# 拉取 git 代码
- name: checkout
uses: actions/checkout@v2
with:
# 拉取对应的分支
ref: ${{ github.event.inputs.branch }}
# fetch-depth 设置为 0,可以获取 git 的所有 commit 历史和 tag。不设置的话默认是只获取最新的一个 commit 信息。
# 获取所有 git commit 是为了生成 changelog
fetch-depth: 0
# 使用前面定义的 node 16 版本
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
# 设置 git user,后面会用该 user 提交代码
- run: git config user.name vitebot
- run: git config user.email vitejs.bot@gmail.com
# 安装 pnpm 和 yarn,pnpm 用于安装依赖,yarn 用于发布 npm 包
- run: npm i -g pnpm@6
- run: npm i -g yarn # even if the repo is using pnpm, Vite still uses yarn v1 for publishing
- run: yarn config set registry https://registry.npmjs.org # Yarn's default registry proxy doesn't work in CI
# 使用 node 16,再次使用是为了指定使用缓存(之前没有安装 pnpm)
# cache 使用方式详情见:https://github.com/actions/setup-node#caching-packages-dependencies
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
# 安装依赖
- name: install
run: pnpm install --frozen-lockfile --prefer-offline
# 创建 .npmrc,用于存储 npm 发布的秘钥,发布时能够自动读取 .npmrc 的秘钥,避免输入密码等用户交互
- name: Creating .npmrc
run: |
cat << EOF > "$HOME/.npmrc"
//registry.npmjs.org/:_authToken=$NPM_TOKEN
EOF
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# 运行【packages/需要发布的包】目录下 package.json 中的 release 脚本,并传入参数
# --quiet 跳过命令行交互的流程,流水线无法进行交互
# --type 传入版本号的生成方式
- name: Release
run: pnpm --dir packages/${{ github.event.inputs.package }} release -- --quiet --type ${{ github.event.inputs.type }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
release 脚本
每个包目录下有一个 package.json,且里面都有一个 release 脚本,例如 vite:
// 节选自 /packages/vite/package.json
{
"name": "vite",
"version": "2.8.1",
"author": "Evan You",
"scripts": {
"release": "ts-node ../../scripts/release.ts"
}
}
实际上是执行了 vite 仓库根目录 scripts 目录下的 release.ts。vite 仓库下的所有包,都是用该脚本进行发布
该文件共 250 行,不多,我们先看看大概结构:
async function main(): Promise<void> {
// 暂时省略
}
main().catch((err) => {
console.error(err)
})
整个文件就是执行 main 函数,如果有错误则输出。
main 函数主要包括以下几个步骤:
- 生成新的版本号 targetVersion
- 二次确认是否发布
- 更新 package.json 版本号
- 执行构建
- 生成 changelog
- 发布 npm 包
- 提交到 GitHub
生成新的版本号
如果执行脚本时,没有指定版本号,则生成新的版本号
生成的规则,是根据命令行参数 --type
生成。行数比较多,其实大部分都是一些错误处理及提示,不必细究。需要知道的是,生成版本号,使用的是 semver 这个 npm 包。
const currentVersion = '1.0.0'
const inc: (i: ReleaseType) => string = (i) =>
semver.inc(currentVersion, i, 'beta')!
inc('major') // 2.0.0,如果第二个参数不是 preXXX(premajor等),则第三个参数会被忽略
inc('premajor') // 2.0.0-beta.0
inc('minor') // 1.1.0
inc('preminor') // 1.1.0-beta.0
inc('patch') // 1.0.1
inc('prepatch') // 1.0.1-beta.0
inc('prerelease') // 1.0.1-beta.0
如果没有传入 --type
,则通过命令行交互,选择版本类型。这种情况发生在手动在项目中调用 pnpm run release
,这也是发布重构前的发布方式。命令行交互方式如下:
下面是代码:
// args 是使用命令行执行该脚本时,传入的参数
// 将第一个参数作为 targetVersion
let targetVersion: string | undefined = args._[0]
// 如果没有传入 targetVersion,则自动生成
if (!targetVersion) {
// 从命令行中读取 type 参数, --type xxx
const type: string | undefined = args.type
// 根据 type 生成版本号
if (type) {
const currentBeta = currentVersion.includes('beta')
if (type === 'next') {
targetVersion = inc(currentBeta ? 'prerelease' : 'patch')
} else if (type === 'stable') {
// Out of beta
if (!currentBeta) {
throw new Error(
`Current version: ${currentVersion} isn't a beta, stable can't be used`
)
}
targetVersion = inc('patch')
} else if (type === 'minor-beta') {
if (currentBeta) {
throw new Error(
`Current version: ${currentVersion} is already a beta, minor-beta can't be used`
)
}
targetVersion = inc('preminor')
} else if (type === 'major-beta') {
if (currentBeta) {
throw new Error(
`Current version: ${currentVersion} is already a beta, major-beta can't be used`
)
}
targetVersion = inc('premajor')
} else if (type === 'minor') {
if (currentBeta) {
throw new Error(
`Current version: ${currentVersion} is a beta, use stable to release it first`
)
}
targetVersion = inc('minor')
} else if (type === 'major') {
if (currentBeta) {
throw new Error(
`Current version: ${currentVersion} is a beta, use stable to release it first`
)
}
targetVersion = inc('major')
} else {
throw new Error(
`type: ${type} isn't a valid type. Use stable, minor-beta, major-beta, or next`
)
}
} else {
// no explicit version or type, offer suggestions
const { release }: { release: string } = await prompts({
type: 'select',
name: 'release',
message: 'Select release type',
choices: versionIncrements
.map((i) => `${i} (${inc(i)})`)
.concat(['custom'])
.map((i) => ({ value: i, title: i }))
})
if (release === 'custom') {
const res: { version: string } = await prompts({
type: 'text',
name: 'version',
message: 'Input custom version',
initial: currentVersion
})
targetVersion = res.version
} else {
targetVersion = release.match(/\((.*)\)/)![1]
}
}
}
二次确认是否发布
二次确认是否发布,beta 版本需要再次确认。
如果传了 --quiet
参数,则跳过,这个用在 GitHub CI 工作流程执行时,执行脚本会主动传入 --quiet
。
if (!args.quiet) {
if (targetVersion.includes('beta') && !args.tag) {
const { tagBeta }: { tagBeta: boolean } = await prompts({
type: 'confirm',
name: 'tagBeta',
message: `Publish under dist-tag "beta"?`
})
if (tagBeta) args.tag = 'beta'
}
const { yes }: { yes: boolean } = await prompts({
type: 'confirm',
name: 'yes',
message: `Releasing ${tag}. Confirm?`
})
if (!yes) {
return
}
} else {
if (targetVersion.includes('beta') && !args.tag) {
args.tag = 'beta'
}
}
更新 package.json 版本号
step('\nUpdating package version...')
updateVersion(targetVersion)
updateVersion 如下,就是覆盖原有的 package.json
function updateVersion(version: string): void {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
pkg.version = version
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
执行构建
执行 pnpm run build
step('\nBuilding package...')
if (!skipBuild && !isDryRun) {
await run('pnpm', ['run', 'build'])
} else {
console.log(`(skipped)`)
}
如果传入参数
--dry
,则表示当次运行是dry run
空运行(又称为试运行),常用于脚本调试在 release.ts 的
dry run
,则不会执行构建和上传 npm 包,但会在命令行打印出将要执行的命令行语句
生成 changelog
执行 pnpm run changelog
,由于篇幅有限,changelog 怎么生成,该文章不做讲解。
step('\nGenerating changelog...')
await run('pnpm', ['run', 'changelog'])
发布 npm 包
step('\nPublishing package...')
await publishPackage(targetVersion, runIfNotDry)
publishPackage 的实现如下:
async function publishPackage(
version: string,
runIfNotDry: RunFn | DryRunFn
): Promise<void> {
// yarn publish 的参数
const publicArgs = [
'publish',
'--no-git-tag-version',
'--new-version',
version,
'--access',
'public'
]
if (args.tag) {
publicArgs.push(`--tag`, args.tag)
}
try {
// important: we still use Yarn 1 to publish since we rely on its specific
// behavior
// 目前仍然是用 yarn 1 进行发布,还没优化
await runIfNotDry('yarn', publicArgs, {
stdio: 'pipe'
})
console.log(colors.green(`Successfully published ${pkgName}@${version}`))
} catch (e: any) {
if (e.stderr.match(/previously published/)) {
console.log(colors.red(`Skipping already published: ${pkgName}`))
} else {
throw e
}
}
}
runIfNotDry 的实现:
// 运行命令行
const run: RunFn = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
type DryRunFn = (bin: string, args: string[], opts?: any) => void
// dry run 模式下,只是输出命令行语句
const dryRun: DryRunFn = (bin, args, opts: any) =>
console.log(colors.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
const runIfNotDry = isDryRun ? dryRun : run
runIfNotDry 在 dry run
模式下,只是输出命令行语句,不执行,仅用于本地调试。
提交到 GitHub
const tag = pkgName === 'vite' ? `v${targetVersion}` : `${pkgName}@${targetVersion}`
const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
// 如果有 git 差异,则提交 commit,并打上标签
// 如果版本号被修改,则 package.json 就会被修改
if (stdout) {
step('\nCommitting changes...')
await runIfNotDry('git', ['add', '-A'])
await runIfNotDry('git', ['commit', '-m', `release: ${tag}`])
await runIfNotDry('git', ['tag', tag])
} else {
console.log('No changes to commit.')
}
step('\nPushing to GitHub...')
await runIfNotDry('git', ['push', 'origin', `refs/tags/${tag}`])
await runIfNotDry('git', ['push'])
if (isDryRun) {
console.log(`\nDry run finished - run git diff to see package changes.`)
}
tag 的生成规则:
以 targetVersion 是 1.0.1 为例,如果发布的包是 vite,则 tag 为 v1.0.1
。如果发布的包是其他的,如 plugin-vue
,则 tag 为 plugin-vue@1.0.1
总结
我们看源码,需要有个目的,例如这次,就是学习 vite 源码的发布方式。这样带着一个目的去看源码,你会发现源码其实并没有多难;切忌对着源码仓库从头到尾看,看不懂,而且很容易会丧失掉学习的耐心和信心。
细心的你,可能会发现,其实很多时候源码也不是十分完美的
-
就例如,release.yml 的注释就明确写明了伪矩阵是为了方便,估计就是从其他的配置文件中复制过来,做了少部分的修改。
-
也例如,发布 npm 包时,用的仍然是 yarn 1(注释中有标明),估计应该是 vite 仓库的包管理工具从 yarn 迁移 pnpm 后,发布方式还没有迁移。
一个开源库能够成为主流,必然有其出色的地方,但毕竟开发者的时间有限,不能做到每方面都是完美的,像这种发布流程,不影响核心代码,没有高的优先级进行优化处理,也是正常。
因此,我们更多的是要从开源代码中学到其精华,将其改进优化,运用到自己的项目中去。
果这篇文章对您有所帮助,请帮忙点个赞👍,您的鼓励是我创作路上的最大的动力