本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
之所以会学习源码,主要是对他能运行这些命令的好奇。这篇源码是尤雨溪自己写的,主要的作用是发布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')
- 我们看到的是引入了minimist,这个是一个轻量级的命令行参数解析引擎。process.argv.slice(2)这个是node提供的原生的一个API,argv返回的是一个数组,数组的第一项是你node的安装文件夹,第二项是你的执行的文件的命令。他这句话的意思是截取从第二个开始直到结束你输入的命令。
- fs是一个node提供读取系统文件的API。
- path是node提供的解析路径的一个API。
- chalk 是一个给cmd文字赋上颜色的一个库。
- semver 用于版本的比较,提供了一系列的API给我们调用,提高开发的效率。
- currentVersion 从package.json里面读取当前的版本信息。
- enquirer是交互式询问用户输入,大家可能还记得js原生的API提供了prompt这样一个方法。
- 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…。