vue3.2的釋出的release.js原始碼

語言: CN / TW / HK

本文參加了由公眾號@若川視野 發起的每週原始碼共讀活動, 點選瞭解詳情一起參與。

之所以會學習原始碼,主要是對他能執行這些命令的好奇。這篇原始碼是尤雨溪自己寫的,主要的作用是釋出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期,連結:https://juejin.cn/post/7084989934699806751