<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入 lerna 發(fā)包機(jī)制 —— lerna version

          共 19577字,需瀏覽 40分鐘

           ·

          2021-09-04 00:48

          最近在公司做的 monorepo 相關(guān)工具開發(fā)的時(shí)候有涉及到這方面的內(nèi)容,于是對這方面進(jìn)行了一些研究。

          lerna 官網(wǎng)關(guān)于 lerna 的描述是這樣的:

          A tool for managing JavaScript projects with multiple packages.

          本質(zhì)上 lerna 是一個(gè)用來管理 monorepo 項(xiàng)目的工具,主要解決了下面的問題:

          將大型代碼倉庫分割成多個(gè)獨(dú)立版本化的 軟件包(package)對于代碼共享來說非常有用。但是,如果某些更改 跨越了多個(gè)代碼倉庫的話將變得很 麻煩 并且難以跟蹤,并且, 跨越多個(gè)代碼倉庫的測試將迅速變得非常復(fù)雜。

          為了解決這些(以及許多其它)問題,某些項(xiàng)目會將 代碼倉庫分割成多個(gè)軟件包(package),并將每個(gè)軟件包存放到獨(dú)立的代碼倉庫中。但是,例如 Babel、 React、Angular、Ember、Meteor、Jest 等項(xiàng)目以及許多其他項(xiàng)目則是在 一個(gè)代碼倉庫中包含了多個(gè)軟件包(package)并進(jìn)行開發(fā)。

          今天我們主要來講解一下關(guān)于 lerna 是怎么去完成一個(gè) monorepo 項(xiàng)目中的發(fā)包操作的。

          lerna 發(fā)包設(shè)計(jì)到兩個(gè)比較關(guān)鍵的指令分別為 lerna version 和 lerna publish 這兩個(gè)指令。

          因?yàn)槠颍覍⒎譃閮蓚€(gè)系列去分別介紹這兩個(gè)指令,本篇文章將從 lerna version 開始,在下篇中我會介紹一下 lerna publish 的工作原理,相信在我介紹完成之后,大家都會對 lerna 的整個(gè)發(fā)包機(jī)制有個(gè)比較清晰的了解。

          lerna version

          根據(jù)官方倉庫的介紹, lerna version 主要的工作為標(biāo)識出在上一個(gè) tag 版本以來更新的 monorepo package,然后為這些包 prompt 出版本,在用戶完成選擇之后修改相關(guān)包的版本信息并且將相關(guān)的變動(dòng) commit 然后打上 tag 推送到 git remote。本質(zhì)上是為一些發(fā)生變動(dòng)的包進(jìn)行了一個(gè) bump version 的操作,當(dāng)然這里用戶也可以將這一過程自動(dòng)化掉,利用 --conventional-commits 這個(gè) api 就可以將這一過程自動(dòng)化掉,常見的場景是可以在寫在一些 CI Config 里面來達(dá)到自動(dòng)發(fā)包的目的。

          同時(shí)它本身還提供了一些相關(guān)的 api,感興趣可以去倉庫或者官方文檔查詢,這里不做詳細(xì)的介紹。

          lerna version 本身在 lerna 中是一個(gè)很重要的指令,有許多其他的指令例如在 lerna 中 two primary commands 之一的 lerna publish 都是基于該指令進(jìn)行相關(guān)的工作。在開始實(shí)現(xiàn)之前先放出一個(gè) lerna version 的整體原理圖。

          那我們直接開始來看一下 lerna version 的具體實(shí)現(xiàn),具體源碼地址為: https://github.com/lerna/lerna/tree/main/commands/version

          lerna 作為一個(gè) monorepo 工具,其本身里面的包也是采用 monorepo 的形式來管理,因此我們可以直接去看 version 的代碼結(jié)構(gòu)為:

          .
          ├── CHANGELOG.md
          ├── README.md
          ├── __tests__ // 測試相關(guān)目錄
          ├── command.js // cli 的入口文件,lerna version 的一些選項(xiàng)都可以在其中看到
          ├── index.js // version 執(zhí)行邏輯相關(guān)的函數(shù)文件
          ├── lib // 相關(guān)的工具函數(shù),根據(jù)名稱可以推斷出其功能
          │ ├── __mocks__
          │ ├── create-release.js
          │ ├── get-current-branch.js
          │ ├── git-add.js
          │ ├── git-commit.js
          │ ├── git-push.js
          │ ├── git-tag.js
          │ ├── is-anything-committed.js
          │ ├── is-behind-upstream.js
          │ ├── is-breaking-change.js
          │ ├── prompt-version.js
          │ ├── remote-branch-exists.js
          │ └── update-lockfile-version.js
          └── package.json

          首先可以在 command.js 文件中看到 lerna version 在 cli 中使用的時(shí)候提供的一些 標(biāo)準(zhǔn) Options 例如 allow-branchconventional-commits 、 ignore-changes 等選項(xiàng)。

          具體的執(zhí)行邏輯在 index.js 這個(gè)目錄中實(shí)現(xiàn)。

          順便一提,這里關(guān)于 lerna 的命令是怎么進(jìn)行分發(fā)以及執(zhí)行的,這部分相關(guān)的邏輯可以參考 @lerna/command 這個(gè)包即源碼目錄 lerna/core/cli/command 中相關(guān)內(nèi)容。lerna monorepo 一些核心相關(guān)概念都在 core 這個(gè)目錄下面,其中會包括一些例如 monorepo 依賴圖的構(gòu)建等內(nèi)容,感興趣可以去研究一下。

          因此現(xiàn)在我們開始看 index.js 相關(guān)的內(nèi)容。

          這個(gè)文件的主要導(dǎo)出了一個(gè)叫做 VersionCommand 類的實(shí)例,相關(guān)的邏輯則在類中實(shí)現(xiàn),大致的結(jié)構(gòu)為:

          class VersionCommand extends Command {
          // 一些 options 的檢查和整合
          configureProperties() {}

          // 初始化
          initialize() {}

          // 執(zhí)行邏輯
          execute() {}

          // 其他部分就是一些類里面的工具函數(shù)例如 getVersionsForUpdates
          // ...
          }

          因此 lerna version 由上面的內(nèi)容可以看出整個(gè)執(zhí)行周期分成三個(gè)部分 設(shè)置屬性 -> 初始化 -> 執(zhí)行

          設(shè)置屬性(configureProperties)

          首先我們先看設(shè)置屬性這一部分,這部分其實(shí)很簡單,它會檢查一些傳遞進(jìn)來的選項(xiàng)是否符合規(guī)范(例如 --create-release 只有和 --conventional-commits 放在一起才能執(zhí)行,否則會報(bào)錯(cuò))。然后把一些 git 相關(guān)的options 全部整合到了一個(gè)叫做 gitOptions 的對象里面去了。這里簡單貼一些代碼參考一下:

          // 檢驗(yàn) --create-release 使用場景
          if (this.createRelease && this.options.conventionalCommits !== true) {
          throw new ValidationError("ERELEASE", "To create a release, you must enable --conventional-commits");
          }
          if (this.createRelease && this.options.changelog === false) {
          throw new ValidationError("ERELEASE", "To create a release, you cannot pass --no-changelog");
          }

          // 這里是把一些用戶傳進(jìn)來的 git 相關(guān)參數(shù)放在 gitOptions 中
          this.gitOpts = {
          // amend 用來跳 lerna version 的 git push 的,默認(rèn)為 true, --amend 用戶傳參數(shù)
          amend,
          commitHooks,
          granularPathspec,
          signGitCommit,
          signGitTag,
          forceGitTag,
          };

          // 用戶傳了 --exact 就不用給 version 設(shè)置 ^ 前綴
          // npm 包的 version 前綴相關(guān)可以參考: https://docs.npmjs.com/misc/config#save-prefix
          this.savePrefix = this.options.exact ? "" : "^";

          初始化(initialize)

          在正式進(jìn)入 bump version 過程中時(shí),會有一個(gè)初始化的過程,初始化過程中首先會檢驗(yàn)一些 monorepo 子 packages 一些 git 倉庫相關(guān)的設(shè)置,拿到需要更新包,然后執(zhí)行到確認(rèn)包的版本信息這一步,有一些相對關(guān)鍵的操作都是在一步完成的,等下我們可以詳細(xì)分析一下:

          大致的執(zhí)行邏輯有:

          initialize() {
          // ...
          // 1. git 倉庫相關(guān)的檢驗(yàn)
          if (this.requiresGit) {
          } else {
          // 跳過 git 倉庫相關(guān)的校驗(yàn),給個(gè)提示
          }

          // 2. 獲取到需要更新的包
          this.updates = collectUpdates();
          // 如果該數(shù)組為空,就停止執(zhí)行
          if (!this.updates.length) {}

          // 3. 包相關(guān)的生命周期函數(shù)(這些函數(shù)是在 lerna.json 中設(shè)置的,可以在 lerna 文檔中找到相關(guān)設(shè)置)
          this.runPackageLifecycle = createRunner(this.options);

          // 4. 將 獲得需要更新版本, 更新版本, 確認(rèn)更新函數(shù)放在一個(gè) tasks 數(shù)組中,之后執(zhí)行
          const tasks = [
          () => this.getVersionsForUpdates(),
          // versions 是由上一步的 getVersionsForUpdates
          versions => this.setUpdatesForVersions(versions),
          () => this.confirmVersions(),
          ];

          // 5. 檢查當(dāng)前 git 的本地工作區(qū)
          if (this.commitAndTag && this.gitOpts.amend !== true) {
          // 這里會把一個(gè)檢查函數(shù)放在 task 函數(shù)的隊(duì)列頂部
          const check = checkUncommittedOnly ? checkWorkingTree.throwIfUncommitted : checkWorkingTree;
          tasks.unshift(() => check(this.execOpts));
          } else {}

          // 6. 執(zhí)行 task 函數(shù)里面內(nèi)容
          return pWaterfall(tasks);
          }

          其實(shí)看這部分邏輯 lerna 這里還是有些功能等著去 TODO 的,感興趣的同學(xué)可以去參加一波貢獻(xiàn)。

          首先我們先從 git 倉庫相關(guān)的檢驗(yàn)看起。首先我們要先知道的是,lerna 管理 monorepo 的一些 workflow 都是基于 git 和 npm 完成的,因此作為一個(gè) lerna 的 monorepo 項(xiàng)目,是需要使用 git 來管理倉庫的。

          這里的 requiresGit 實(shí)際上是 versionCommand 類的一個(gè) get 方法,返回的是一個(gè) git 相關(guān)的值,存在即可:

          // 只要這些參數(shù)存在其中之一就證明目前是需要驗(yàn)證 git 相關(guān)的內(nèi)容
          get requiresGit() {
          return (
          this.commitAndTag || this.pushToRemote || this.options.allowBranch || this.options.conventionalCommits
          );
          }

          requiresGit 內(nèi)部的檢驗(yàn)邏是要發(fā)生在計(jì)算出需要更新的包以及版本選擇 之前的(當(dāng)然這兩步也是在 initialize() 之前完成的)。

          具體的檢驗(yàn)邏輯:

          • 校驗(yàn)本地是否有沒有被 commit 內(nèi)容

          • 判斷當(dāng)前的分支是否正常

          • 判斷當(dāng)前分支是否在 remote 存在

          • 判斷當(dāng)前分支是否在 lerna.json 允許的 allowBranch 設(shè)置之中

          • 判斷當(dāng)前分支提交是否落后于 remote

          這里相關(guān)的操作一些判斷操作實(shí)際上都借用了 git 相關(guān)的命令來完成,就不一一去說明這里的 api 是如何使用的了:例如獲取當(dāng)前分支,實(shí)際上就是獲取到 git 命令的執(zhí)行結(jié)果:

          function currentBranch(opts) {
          log.silly("currentBranch");

          const branch = childProcess.execSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], opts);
          log.verbose("currentBranch", branch);

          return branch;
          }

          本質(zhì)上就是執(zhí)行了一次 git rev-parse --abbrev-ref HEAD 獲取到當(dāng)前分支名稱。

          其他 git 相關(guān)的判斷都是和此類似,這里就不做具體的贅述,這里如果有相關(guān)的需求是可以在這里學(xué)習(xí)一下相關(guān)工具函數(shù)的使用的,里面有一些平時(shí)不常用的 git 命令,個(gè)人覺得 lerna 這里的工具代碼寫的還是不錯(cuò)的。

          下面就直接去看獲取到更新包這部分的邏輯:

          this.updates = collectUpdates(
          this.packageGraph.rawPackageList,
          // 項(xiàng)目圖
          this.packageGraph,
          // 執(zhí)行 git 命令的一些 option 有 maxBuffer 和 cwd 兩個(gè)參數(shù)
          this.execOpts,
          // cli 的一些 參數(shù) 以及 lerna.json 里面的一些配置和環(huán)境變量等
          this.options
          ).filer(node => {
          // 這里會把滿足 pkg.json 里面設(shè)置了 private: true 且傳遞了 --no-private 參數(shù)的子package 跳過更新
          if (node.pkg.private && this.options.private === false) {
          // 這里 --no-privte 應(yīng)該是個(gè)默認(rèn)行為,這是可以給 lerna 來個(gè) pr 的
          return false;
          }

          // 當(dāng)前包沒有 version 參數(shù) 也會做個(gè) judge
          if (!node.version) {
          // pkg 的 pkg.json 里面沒設(shè)置 version 參數(shù),但設(shè)置了 private 還是可以跳的
          if (node.pkg.private) {
          } else {
          // 拋錯(cuò)
          }
          }

          // 把有 version 的篩出去
          return !!node.version;
          })

          這里我們直接去看 collectUpdates 的是怎么拿到需要更新的包的信息的,collectUpdates 之后的 filter 操作寫上了注釋,之后不做具體介紹,可以自行參考對照。

          function collectUpdates(filteredPackages, packageGraph, execOpts, commandOptions) {
          // ...
          // forced 是需要強(qiáng)制發(fā)布的包名的一個(gè) Set 對象
          const forced = getPackagesForOption(useConventionalGraduate ? conventionalGraduate : forcePublish);
          // 獲取到當(dāng)前 monorepo 下的 packageList 是個(gè)以package name為key的map對象
          const packages =
          filteredPackages.length === packageGraph.size
          ? packageGraph
          : new Map(filteredPackages.map(({ name }) => [name, packageGraph.get(name)]));

          // --since <ref> 參數(shù) lerna version 這邊為 undefined
          let committish = commandOptions.since;

          if (hasTags(execOpts)) {
          // 獲取到上次 附注tag(即 git tag -a "v3" -m "xx" 創(chuàng)建的 tag)
          const { sha, refCount, lastTagName } = describeRef.sync(execOpts, commandOptions.includeMergedTags);

          // 上一次 tagCommit 到當(dāng)前的提交數(shù)量為 refCount
          if (refCount === "0" && forced.size === 0 && !committish) {
          log.notice("", "Current HEAD is already released, skipping change detection.");
          return [];
          }

          // 這個(gè)是個(gè)測試版本的選項(xiàng)
          if (commandOptions.canary) {
          committish = `${sha}^..${sha}`;
          } else if (!committish) {
          // 如果這個(gè)地方連 tag 都沒有的話, committish 就會成 undefined, 之后就會用初始化的 commit 來算
          committish = lastTagName;
          }
          }

          // 用戶使用了 --conventional-commits --conventional-graduate 這個(gè)選項(xiàng)
          if (useConventionalGraduate) {
          // 會把所有預(yù)發(fā)布的包更新成正式包版本,這里只是給個(gè) log
          if (forced.has("*")) {
          // --force-publish 就發(fā)布所有
          log.info("", "Graduating all prereleased packages");
          } else {
          log.info("", "Graduating prereleased packages");
          }
          } else if (!committish || forced.has('*')) {
          // 使用了 --force-publish 或者 tag 沒有找到(之前沒有使用過lerna發(fā)包),就會當(dāng)成所有包需要更新
          log.info("", "Assuming all packages changed");

          // 這個(gè) collectPackages 會有三個(gè)參數(shù),還有個(gè) isCandidate 用來添加篩選條件判斷哪些包需要更新
          // 這里 isCandidate 沒傳意味著傳進(jìn)去的 packages 都要更新
          return collectPackges(packages, {
          onInclude: name => log.verbose("updated", name),
          // 這個(gè) excludeDependents 是由 --no-private 參數(shù)來決定去 exclude 掉一些 private: true 的包
          excludeDependents,
          })
          }

          // 下面就是正常的收集情況
          // ...
          return collectPackages(packages, {
          // 這里相比較于上面就多了一個(gè) isCandidate, 只有符合下面 isForced(強(qiáng)制更新), needsBump(跳過一些之前沒有被 prereleased 的包)
          // hasDiff(有改動(dòng)的包,在這里 ignoreChanges 會生效一波)
          isCandidate: (node, name) => isForced(node, name) || needsBump(node) || hasDiff(node),
          onInclude: name => log.verbose("updated", name),
          excludeDependents,
          });
          }

          根據(jù)上面的代碼層次我們其實(shí)可以比較清晰的看到 collectUpdates 是怎么收集到需要更新的 package 的,首先會從 core 那邊拿到當(dāng)前 monorepo 項(xiàng)目下面的一些 package,然后根據(jù) 一些選項(xiàng) (例如 --force-publish 和 --conventional-commit)以及項(xiàng)目本身 commit 的 tag 信息去得到當(dāng)前 monorepo 項(xiàng)目下需要更新的包的一個(gè) updates 數(shù)組。這里有個(gè)細(xì)節(jié)是這里的 lerna 去獲取 tag 的時(shí)候,使用的是 git describe 命令,而且它附帶的一些參數(shù)說明(可以去看下這個(gè) describe-ref.js),這里獲取到的 tag 是 annotated tag

          這個(gè) updates 數(shù)據(jù)將后續(xù)作為bump version 的一系列操作。

          在獲取完 updates 數(shù)組后,初始化過程這個(gè)時(shí)候來到了執(zhí)行 runLifeCycle 函數(shù)這一步,這一步就是用于執(zhí)行 lerna.json 里面用戶設(shè)置的一些生命周期函數(shù),這里不做太多的講解。想知道具體的執(zhí)行函數(shù)代碼怎么寫的可以參考 @lerna/run-lifecycle 這個(gè)庫函數(shù)。

          然后就到了初始化的執(zhí)行的一些步驟,這里會構(gòu)建一個(gè) tasks 隊(duì)列,用來執(zhí)行

          // 這里的一些 version 函數(shù)都是根據(jù)之前獲取到的 updates 數(shù)組來進(jìn)行相關(guān)的工作
          const tasks = [
          // 獲得需要更新的 version
          () => this.getVersionsForUpdates(),
          // 將 versions 設(shè)置上去,這里執(zhí)行的時(shí)候是個(gè) reduce 過程,實(shí)際上是把 上一步函數(shù)返回的 versions 值拿到了返回出來
          versions => this.setUpdatesForVersions(versions),
          // 確認(rèn)更新
          () => this.confirmVersions(),
          ];

          // 中間有個(gè)根據(jù)選項(xiàng)在 tasks 隊(duì)列頂端插入 check 函數(shù),檢查一下當(dāng)前 git 的 working tree 是否正常
          // ...

          // 按照 reduce 去執(zhí)行任務(wù)隊(duì)列中的方法
          return pWaterfall(tasks);

          這里就是初始化的最后一個(gè)步驟來,我們主要看 tasks 隊(duì)列中的三個(gè)方法的執(zhí)行細(xì)節(jié)。

          這里 getVersionsForupdates 過程就是這個(gè) prompt 過程,最后得到一個(gè)以之前 updates 數(shù)組中的包名為 key,用戶選擇的 version 作為 value 的一個(gè) Map 對象。

          當(dāng)然這里我列舉的只是一般用戶使用的情況,如果選擇了 conventional-commits 那么這一步就是自動(dòng)幫用戶生成對應(yīng)對應(yīng)的包版本了,具體的執(zhí)行細(xì)節(jié)可以參考(下面會涉及到一些 options,這里建議可以結(jié)合 lerna version 文檔中的一些 api 介紹來閱讀)。

          if (repoVersion) {
          // 這里是用戶傳了 lerna version [] 后面數(shù)組里面的 semver bump 參數(shù),可以直接跳掉 prompt 過程,然后對對應(yīng)的版本去直接 bump
          // 這里對應(yīng)的是正式的 released 版本
          predicate = makeGlobalVersionPredicate(repoVersion);
          } else if (increment && independentVersions) {
          // increament 相較于 repoVersion 的區(qū)別在于這里對應(yīng)的是非正式版本的包,處于一個(gè) prereleased 階段
          // 過程和上面是差不多的,這里對應(yīng)的是所有的 indepent 包(即用戶在 lerna.json 里面設(shè)置了 indepent)
          predicate = node => semver.inc(node.version, increment, resolvePrereleaseId(node.prereleaseId));
          } else if (increment) {
          // 這里對應(yīng)的是 fixed 的包 prereleased 版本更新
          } else if (conventionalCommits) {
          // 如果是 --conventional-commits 這里就直接幫用戶生成了,不用prompt了
          return this.recommendVersions(resolvePrereleaseId);
          } else if (independentVersions) {
          // 這個(gè)就是對正常的 indepent 包進(jìn)行 prompt 的一個(gè)過程
          predicate = makePromptVersion(resolvePrereleaseId);
          } else {
          // fixed 包進(jìn)行 prompt
          // ...
          }

          // 上面的 predicate 是個(gè)返回 newversion 的函數(shù)
          // 會放到 getVersion 即其返回的 newVersion,然后再用 reduceVersions 這個(gè)方法去生成上面我提到的 Map 對象
          // reduceVersion 也是一個(gè)典型的 reduce 應(yīng)用(即上一步的結(jié)果可以用于下一步的函數(shù)執(zhí)行)
          return Promise.resolve(predicate).then(getVersion => this.reduceVersions(getVersion));

          講完 getVersionsForUpdates() ,下面就到了 setUpdatesForVersions 這個(gè)過程,這一步操作就相對簡單,拿到上一次的 versions Map 對象,用來做一次預(yù)處理,這一步會得到一個(gè) updateVersions (本質(zhì)上其實(shí)就是上一次拿到的 versions)和一個(gè) packagesToVersion(本質(zhì)上就是 updates 這個(gè)數(shù)組),這里相當(dāng)于是做了一下整合。

          setUpdatesForVersions(versions) {
          if (this.project.isIndependent() || versions.size === this.packageGraph.size) {
          // 只需要檢查部分固定的版本
          this.updatesVersions = versions;
          } else {
          let hasBreakingChange;
          for (const [name, bump] of versions) {
          // 這里直接看代碼知道 breakChange 的判斷比我介紹要直觀多,代碼在下面
          hasBreakingChange = hasBreakingChange || isBreakingChange(this.packageGraph.get(name).version, bump);
          }
          if (hasBreakingChange) {
          this.updates = Array.from(this.packageGraph.values());
          // 把 private 設(shè)成 true 的包篩出去
          if (this.options.private === false) {
          this.updates = this.updates.filter(node => !node.pkg.private);
          }
          this.updatesVersions = new Map(this.updates.map(node => [node.name, this.globalVersion]));
          } else {
          this.updatesVersions = versions;
          }
          }

          this.packagesToVersion = this.updates.map(node => node.pkg);
          }

          上面判斷 breackChange 的方法為:

          // releaseType 能判斷是包的哪一位發(fā)生了變化,這里的判斷其實(shí)有些迷惑行為.jpg
          const releaseType = semver.diff(currentVersion, nextVersion);
          let breaking;
          if (releaseType === "major") {
          // self-evidently
          breaking = true;
          } else if (releaseType === "minor") {
          // 0.1.9 => 0.2.0 is breaking
          // lt 是小于
          breaking = semver.lt(currentVersion, "1.0.0");
          } else if (releaseType === "patch") {
          // 0.0.1 => 0.0.2 is breaking(?)
          breaking = semver.lt(currentVersion, "0.1.0");
          } else {
          // versions are equal, or any prerelease
          breaking = false;
          }

          最后一步 confirmVersion 其實(shí)就是把上一步 setUpdatesForVersions 生成的 packagesToVersion 的值展示出來,讓用戶確定之前的選擇是否正確,然后返回一個(gè) bool 值(即用戶選擇的是否)。之后在執(zhí)行階段再去根據(jù)初始化時(shí)的這個(gè)結(jié)果去做一些更新之類的操作。

          那么當(dāng)這里整個(gè)初始化的過程就結(jié)束了,這里吐槽一下其實(shí)初始化這里的很多放到 execute 這個(gè)函數(shù)中去會比較好一點(diǎn)。因?yàn)槌跏蓟@里其實(shí)已經(jīng)完成 lerna version 的大部分功能了,最后執(zhí)行其實(shí)本質(zhì)上執(zhí)行的也只是一個(gè)更新 pkg 中的 version 的操作。

          執(zhí)行(execute)

          到這里麻煩讀者可以再回到文章開頭看一下 lerna version 的整體執(zhí)行過程,到現(xiàn)在我們已經(jīng)完成了 version 的更新操作,拿到了一些相關(guān)參數(shù)。最后剩下的幾步就是在需要修改的 pkg 里面對應(yīng)的包的版本,然后將修改 commit 并打上 tag 推送到 git remote。

          這幾個(gè)步驟就是在執(zhí)行這里完成。

          這里的過程比較簡單,和 initialize() 一樣,這里初始化了一個(gè) task 數(shù)組,然后把相關(guān)的操作函數(shù)放到 task 最后用 p-reduce 去 run。

           execute(){
          // 放入更新包版本的函數(shù)
          const tasks = [() => this.updatePackageVersions()];
          // 用戶可以通過設(shè)置 --no-git-tag-version 來跳過 tag 和 commit 的過程
          // 這個(gè) commitAndTag 默認(rèn)為 true
          if (this.commitAndTag) {
          tasks.push(() => this.commitAndTagUpdates());
          } else {
          this.logger.info("execute", "Skipping git tag/commit");
          }

          // pushToRemote 受到 commitAndTag 和 ammend 參數(shù)的共同影響
          // 用戶可以單獨(dú)通過 --amend 來讓 lerna version 最后不 push
          if (this.pushToRemote) {
          tasks.push(() => this.gitPushToRemote());
          } else {
          this.logger.info("execute", "Skipping git push");
          }

          // 這個(gè) createRelease 使用來設(shè)置特定平臺的 release發(fā)布的,配合 --conventional-commits 來一起使用
          // 這個(gè)不做詳細(xì)的介紹
          if (this.createRelease) {
          this.logger.info("execute", "Creating releases...");
          tasks.push(() =>
          createRelease(
          this.options.createRelease,
          { tags: this.tags, releaseNotes: this.releaseNotes },
          { gitRemote: this.options.gitRemote, execOpts: this.execOpts }
          )
          );
          } else {
          this.logger.info("execute", "Skipping releases");
          }

          // pWaterfall 這個(gè)方法就是封裝了一下 p-reduce 這個(gè)庫函數(shù)
          return pWaterfall(tasks).then(() => {
          // 這里的 composed 是用來標(biāo)記當(dāng)前是 lerna version 還是 lerna publish 在執(zhí)行
          if (!this.composed) {
          // lerna version 就結(jié)束這個(gè)過程
          this.logger.success("version", "finished");
          }
          // 如果是 lerna publish 就耀把這些參數(shù)返回出去
          return {
          updates: this.updates,
          updatesVersions: this.updatesVersions,
          };
          });
          }

          正常邏輯上會按照 更新包的版本 -> 生成 commit 和打 tag -> 提交到 remote這樣一個(gè)過程來執(zhí)行,參考 task 的順序就行。

          先看 updatePackageVersions 這個(gè)方法,這個(gè)方法會幫我們 更新一下包的版本,修改掉文件里面的版本,并且將更新 git add 添加到暫緩區(qū),如果是使用了 --conventional-commits,也會在這一步幫我們生成 CHANGELOG 或者是更新 CHANGELOG。這一步會用一個(gè) changeFiles 來記錄修改文件的 path。

          大致的代碼結(jié)構(gòu)是這樣的:

          updatePackageVersions() {
          const { conventionalCommits, changelogPreset, changelog = true } = this.options;
          const independentVersions = this.project.isIndependent();
          const rootPath = this.project.manifest.location;
          // 記錄修改的文件
          const changedFiles = new Set();

          // 這里的一系列鏈?zhǔn)秸{(diào)用以及異步方法都是用的 promise,作者甚至在源碼這里吐槽想用 async/await...
          let chain = Promise.resolve();

          // 這里的 action 其實(shí)也是個(gè) reduce 執(zhí)行過程,即前一步的函數(shù)會把結(jié)果給后面
          const actions = [
          // .. 里面是一堆和 pkg 更新有關(guān)的函數(shù)
          // 在這一步會有個(gè)函數(shù)更新掉版本
          pkg => {
          // 把新版本設(shè)置上
          pkg.set("version", this.updatesVersions.get(pkg.name));

          // 與 pkg 有關(guān)的依賴(也要跟著一起更新)
          // 這里也是從 core 那邊拿到的依賴圖,然后可以得到一個(gè) pkg 依賴了哪些 lerna 內(nèi)部的庫,那些內(nèi)部庫也要跟著更新
          for (const [depName, resolved] of this.packageGraph.get(pkg.name).localDependencies) {
          const depVersion = this.updatesVersions.get(depName);
          if (depVersion && resolved.type !== "directory") {
          // don't overwrite local file: specifiers, they only change during publish
          pkg.updateLocalDependency(resolved, depVersion, this.savePrefix);
          }
          }
          // 更新 pkg-lock.json 中版本,并且將修改反映到 pkg 文件中(pkg.serialize())
          // 然后 then 的時(shí)候?qū)⑿薷挠涗浵聛碓?changeFiles 中
          return Promise.all([updateLockfileVersion(pkg), pkg.serialize()]).then(// ...);
          }
          // ..
          ];

          // 如果是 --conventional-commits 則會幫我們自動(dòng)生成 CHANGELOG
          // 下面的 updateChangelog 方法也是可以看到的
          if (conventionalCommits && changelog) {
          // we can now generate the Changelog, based on the
          // the updated version that we're about to release.
          const type = independentVersions ? "independent" : "fixed";

          actions.push(pkg =>
          ConventionalCommitUtilities.updateChangelog(pkg, type, {
          changelogPreset,
          rootPath,
          tagPrefix: this.tagPrefix,
          }).then(({ logPath, newEntry }) => {
          // commit the updated changelog
          changedFiles.add(logPath);
          // 添加 released notes
          if (independentVersions) {
          this.releaseNotes.push({
          name: pkg.name,
          notes: newEntry,
          });
          }
          return pkg;
          })
          );
          }
          // pPipe 是封裝的一個(gè)按照 reduce 順序執(zhí)行異步函數(shù)的方法
          // 這里會把前面的 action 一一執(zhí)行,得到最后一步返回的結(jié)果
          const mapUpdate = pPipe(actions);

          // 按照拓?fù)漤樞蛉?zhí)行前面拿到的一些需要更新的包,以及需要更新的 pathFile
          chain = chain.then(() =>
          runTopologically(this.packagesToVersion, mapUpdate, {
          concurrency: this.concurrency,
          rejectCycles: this.options.rejectCycles,
          })
          );

          // fixed 類型的 monorepo 項(xiàng)目的更新,會多一個(gè) lerna.json 修改
          // 因?yàn)橐薷睦锩娴陌姹荆@里也會和上面一樣有 --conventional-commits 的判斷,不做過多講解
          if (!independentVersions) {}

          // 前面有說這個(gè)值默認(rèn)是 true,這里會把修改 git add 到緩存區(qū)
          if (this.commitAndTag) {
          chain = chain.then(() => gitAdd(Array.from(changedFiles), this.gitOpts, this.execOpts));
          }

          return chain;
          }

          然后再讓我們看生成 commit 這個(gè)過程,這一步步驟就比較簡單,將前面 add 到緩存區(qū)的修改 commit 并且打上 tag。相關(guān)邏輯在 commitAndTagUpdates 這個(gè)方法上,這一步代碼比較簡單,可以直接看一下。

          commitAndTagUpdates() {
          let chain = Promise.resolve();

          // 這一步會完成 git add . 和 git tag 的操作,commit 的 message 可以用戶自行設(shè)置或者 lerna 會幫你自動(dòng)生成
          if (this.project.isIndependent()) {
          chain = chain.then(() => this.gitCommitAndTagVersionForUpdates());
          } else {
          chain = chain.then(() => this.gitCommitAndTagVersion());
          }

          chain = chain.then(tags => {
          this.tags = tags;
          });
          // ...

          return chain;
          }

          然后最后一步就是將 commit 以及 tag push 到 remote,這一步更簡單,lerna 封裝了一個(gè) git push 方法,推送到當(dāng)前的分支的 remote 上去。

          gitPushToRemote() {
          this.logger.info("git", "Pushing tags...");

          // gitPush 簡單封裝了一下 git push 命令
          return gitPush(this.gitRemote, this.currentBranch, this.execOpts);
          }

          總結(jié)

          本文主要從源碼的角度介紹了一下 lerna version 的工作原理,在使用 lerna 進(jìn)行發(fā)包相關(guān)的操作的時(shí)候,lerna version 起到了一個(gè)很重要的作用,如果單獨(dú)使用 lerna publish,一般選項(xiàng)情況下也是會首先進(jìn)到 lerna version 這邊走完整個(gè) bump version 的操作,最后再去進(jìn)行發(fā)包,因此 lerna version 是 lerna 進(jìn)行 monorepo 發(fā)包的一個(gè)基礎(chǔ)。

          下一篇文章將講解一下,lerna publish 做了哪一些工作,是怎么完成將相關(guān)的包發(fā)布的操作的。

          瀏覽 147
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色色视频免费看 | 人人射人人干 | 天天日天天射一区二区三区 | 中文字幕18页 | www色老板 |