深入理解 Babel - 項目管理工具 lerna 解析
一
背景
Babel 是一個比較龐大的項目,其子工程就有至少 140 個 (如 babel/plugins/presets/lerna/babel-loader 等),產出的子工具已經是前端開發的基礎設施,對開發效率、代碼質量等有非常高的要求。
在本文中,我們將瞭解 Babel 是怎樣進行項目管理的。
本文從工程管理、代碼管理、文檔管理、質量管理四個方面對 Babel 項目管理進行拆解分析。
工程管理
Babel 是典型的 monorepo 項目,即單倉庫項目,所有的子模塊在同一個倉庫裏。Babel 目前有 140 + 個子模塊,在工程管理部分,需要解決以下問題:
-
模塊間如何方便地互相關聯進行本地開發;
-
整個項目的版本控制;
-
操作自動化。
工程管理部分主要使用 lerna、yarn 等工具。
代碼風格
Babel 是多人協作開發的開源項目,如何保證代碼風格一致,Babel 使用的是社區常見的解決方案。
該模塊主要使用 eslint、prettier 等工具。
文檔
Babel 的迭代速度很快、涉及的模塊很多,該模塊解決版本發佈後如何自動更新相關文檔等問題。
該模塊主要使用 lerna 等工具。
質量控制
Babel 的產品是前端開發的基礎設施,該模塊主要保證 Babel 的產出是高質量的。
該模塊主要使用 jest、git blame 等工具。
二
monorepo
Babel 使用 monorepo 模式進行工程管理。
什麼是 monorepo
monorepo(monolithic repository),指的是單倉庫代碼,將多個項目代碼存儲在一個倉庫裏。另外有一種模式是 multirepo,指的是多倉庫代碼 (one-repository-per-module),不同的項目分佈在不同的倉庫裏。React、Babel、Jest、Vue、Angular 均採用 monorepo 進行項目管理。
典型的 monorepo 結構是:
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
這是 Babel 源碼的目錄結構:
├─ lerna.json
├─ package.json
└─ packages/ # 這裏將存放所有子項目目錄
├─ README.md
├─ babel-cli
├─ babel-code-frame
├─ babel-compat-data
├─ babel-core
├─ babel-generator
├─ babel-helper-annotate-as-pure
├─ babel-helper-builder-binary-assignment-operator-visitor
├─ babel-helper-builder-react-jsx
├─ ...
而 rollup 則採取了 multirepo 的模式:
├─ rollup
├─ package.json
├─ ...
├─ plugins
├─ package.json
├─ ...
├─ awesome
├─ package.json
├─ ...
├─ rollup-starter-lib
├─ package.json
├─ ...
├─ rollup-plugin-babel
├─ package.json
├─ ...
├─ rollup-plugin-commonjs
├─ package.json
├─ ...
monorepo 的優缺點
優點
-
便捷的代碼複用與依賴管理
當所有項目代碼都在一個工程裏,抽離可複用的代碼就十分容易了。並且抽離後,如果複用的代碼有改動,可以通過一些工具,快速定位受影響的子工程,進而做到子工程的版本控制。
-
便捷的代碼重構
通過一些工具,monorepo 項目中的代碼改動可以快速地定位出代碼變更的影響範圍,對整個工程進行快速的整體測試。而如果子工程分散在不同的工程分支裏的話,通用代碼的重構將難以觸達各個子工程。
-
倡導開放、共享
monorepo 項目中,開發者可以方便地看到所有子工程,這樣響應了 "開放、共享" 的組織文化。可以激發開發者對工程質量等維護的熱情(畢竟別人看不到自己的代碼,亂不亂就看自己心情了),有助於團隊建立良好的技術氛圍。
缺點
-
複雜的權限管理
因爲所有子工程集中在一個工程裏,某些子工程如果不希望對外展示的話,monorepo 的權限管理就比較難以實現了,難以鎖定目標工程做獨立的代碼權限管理。
-
較高的熟悉成本
相對於 multirepo,monorepo 涉及各種子工程、通用依賴等,新的開發者在理解整個項目時,可能需要了解較多的信息才能入手,如通用依賴代碼、各子工程功能。
-
較大的工程體積
很明顯,所有子工程集成在一個工程裏,代碼體積會非常大,對文件存儲系統等提出了較高的要求。
-
較高的質量風險
成也蕭何敗蕭何,monorepo 提供了便捷的代碼複用能力,同時,一個公用模塊的某個版本有 bug 的話,很容易影響所有用到它的子工程。此時,做好高覆蓋率的單元測試就比較重要了。
選擇
multirepo 和 monorepo 是兩種不同的理念。
multirepo 允許多元化發展,每個模塊獨立實現自己的構建、單元測試、依賴管理等。monorepo 抹平了模塊間的很多差異,通過集中管理和高度集成化的工具,減少開發和溝通時的成本。monorepo 最大的問題可能就是不能管理佔用空間太大的項目了。
所以,還是要根據項目的實際需求出發選擇用哪種項目管理模式。
三
lerna
lerna 是基於 git/npm/yarn 等的工作流提效工具,用於維護 monorepo。它是 Babel 開發過程中提升開發效率產出的工具。
lerna 本身也是一個 monorepo 的項目,並且,lerna 爲 monorepo 項目提供瞭如下支持:
-
項目管理
lerna 提供了一系列命令用於 monorepo 項目初始化、添加子項目、查看項目信息等。
-
依賴管理
lerna 支持爲 monorepo 項目統一管理公共依賴、自動安裝各個子項目的依賴、自動創建子模塊符號鏈接等。
-
版本管理
lerna 可以根據項目代碼的變動情況,發現影響的子項目範圍,在發佈時提供語義化版本推薦等,極大提升了 monorepo 項目的版本管理效率。
lerna 命令集
命令行列表
lerna 官網有對各種命令各種用法的詳細介紹,這些命令可以分爲:項目管理、依賴管理、版本管理三大類。
全局配置項
lerna 有一批通用參數,所有子命令均可以使用。
--concurrency
當 lerna 將任務並行執行時,需要使用多少線程 (默認爲邏輯 CPU 內核數)。
lerna publish --concurrency 1
--loglevel<silent|error|warn|success|info|verbose|silly>
要報告什麼級別的日誌。如果失敗,所有日誌都寫到當前工作目錄中的 lerna-debug.log 中。
任何高於該設置的日誌都會顯示出來。默認值是 "info"。
--max-buffer
爲每個底層進程調用設置的最大緩衝區長度。例如,當有人希望在運行 lerna import 的同時導入包含大量提交的倉庫時,就是它出場的時候了。在這種情況下,內置的緩衝區長度可能不夠。
--no-progress
禁用進度條。在 CLI 環境中總是這樣。
--no-sort
默認情況下,所有任務都按照拓撲排序的順序在包上執行,以尊重所討論的包的依賴關係。在不保證 lerna 調用一致的情況下,以最努力的方式打破循環。
如果只有少量的包有許多依賴項,或者某些包執行的時間長得不成比例,拓撲排序可能會導致併發瓶頸。--no-sort 配置項禁用排序,而是以最大併發的任意順序執行任務。
如果您運行多個 watch 命令,該配置項也會有所幫助。因爲 lerna run 將按照拓撲排序的順序執行命令,所以在繼續執行之前可能會等待某個命令。當您運行 "watch" 命令時會阻塞執行,因爲他們通常不會結束。
--reject-cycles
如果(在 bootstrap、exec、publish 或 run 中)發現循環,則立即失敗。
lerna bootstrap --reject-cycles
過濾器參數
--scope
只包含名稱與給定通配符匹配的包。
lerna exec --scope my-component -- ls -la
lerna run --scope toolbar-* test
lerna run --scope package-1 --scope *-2 lint
--ignore
排除名稱與給定通配符匹配的包。
lerna exec --ignore package-{1,2,5} -- ls -la
lerna run --ignore package-1 test
lerna run --ignore package-@(1|2) --ignore package-3 lint
--no-private
排除私有的包。默認情況下是包含它們的。
--since [ref]
只包含自指定 ref 以來已經改變的包。如果沒有傳遞 ref,它默認爲最近的標記。
# 列出自最新標記以來發生變化的包的內容
$ lerna exec --since -- ls -la
# 爲自“master”以來所有發生更改的包運行測試
$ lerna run test --since master
# 列出自“某個分支”以來發生變化的所有包
$ lerna ls --since some-branch
在 CI 中使用時,如果您可以獲得 PR 將要進入的目標分支,那麼它將特別有用,因爲您可以將其作爲 --since 配置項的 ref。這對於進入 master 和 feature 分支的 PR 來說很有效。
--exclude-dependents
當使用 --since 運行命令時,排除所有傳遞的被依賴項,覆蓋默認的 “changed” 算法。
如果沒有 --since 該參數時無效的,會拋出錯誤。
--include-dependents
在運行命令時包括所有傳遞的被依賴項,無視 --scope、--ignore 或 --since。
--include-dependencies
在運行命令時包括所有傳遞依賴項,無視 --scope、--ignore 或 --since。
與接受 --scope(bootstrap、clean、ls、run、exec) 的任何命令組合使用。確保對任何作用域的包(通過 --scope 或 --ignore)的所有依賴項(和 dev 依賴項)也進行操作。
注意:這將會覆蓋 --scope 和 --ignore。
例如,如果一個匹配了 --ignore 的包被另一個正在引導的包所以來,那麼它仍會照常工作。
當您想要 “設置” 一個依賴於其他正在設置的包其中的一個包時,這是非常有用的。
lerna bootstrap --scope my-component --include-dependencies
# my-component 及其所有依賴項將被引導
lerna bootstrap --scope "package-*" --ignore "package-util-*" --include-dependencies
# 所有匹配 "package-util-*" 的包將被忽略,除非它們依賴於名稱匹配 "package-*" 的包
--include-merged-tags
lerna exec --since --include-merged-tags -- ls -la
在使用 --since 命令時,它包含來自合併分支的標記。這隻有在從 feature 分支進行大量發佈時纔有用,通常情況下不推薦這樣做。
lerna 原理解析
文件結構
以下是 lerna 的主要目錄結構(省略了一些文件和文件夾):
lerna
├─ CHANGELOG.md -- 更新日誌
├─ README.md -- 文檔
├─ commands -- 核心子模塊
├─ core -- 核心子模塊
├─ utils -- 核心子模塊
├─ lerna.json -- lerna 配置文件
├─ package-lock.json -- 依賴聲明
├─ package.json -- 依賴聲明
├─ scripts -- 內置腳本
└─ yarn.lock -- 依賴聲明
有趣的是,lerna 本身也是用 lerna 進行開發管理的。它是一個 monorepo 項目,其各個子項目分佈在 lerna/commands/、core/、utils/* 目錄下。
另外,在源碼中,經常會看到名稱爲 @lerna/command 的子項目,如 lerna/core/lerna/index.js 的內容是:
const cli = require("@lerna/cli");
const addCmd = require("@lerna/add/command");
const bootstrapCmd = require("@lerna/bootstrap/command");
const changedCmd = require("@lerna/changed/command");
const cleanCmd = require("@lerna/clean/command");
const createCmd = require("@lerna/create/command");
const diffCmd = require("@lerna/diff/command");
const execCmd = require("@lerna/exec/command");
const importCmd = require("@lerna/import/command");
const infoCmd = require("@lerna/info/command");
const initCmd = require("@lerna/init/command");
const linkCmd = require("@lerna/link/command");
const listCmd = require("@lerna/list/command");
const publishCmd = require("@lerna/publish/command");
const runCmd = require("@lerna/run/command");
const versionCmd = require("@lerna/version/command");
這些子項目分佈在 lerna/commands/、core/、utils/* 下,截至本文截稿時,lerna 有 61 個子項目。
以下是各子項目分佈:
命令行註冊
lerna 命令註冊工作集中在 lerna/core/lerna/* 路徑下。
lerna/core/lerna/package.json
該文件的 bin 字段定義了 lerna 命令:
"bin": {
"lerna": "cli.js"
}
lerna/core/lerna/cli.js
該文件描述了命令行的執行入口:
#!/usr/bin/env node
"use strict";
/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
require(".")(process.argv.slice(2));
}
lerna/core/lerna/index.js
該文件爲命令行引入了所有 lerna 指令:
"use strict";
const cli = require("@lerna/cli");
const addCmd = require("@lerna/add/command");
const bootstrapCmd = require("@lerna/bootstrap/command");
const changedCmd = require("@lerna/changed/command");
const cleanCmd = require("@lerna/clean/command");
const createCmd = require("@lerna/create/command");
const diffCmd = require("@lerna/diff/command");
const execCmd = require("@lerna/exec/command");
const importCmd = require("@lerna/import/command");
const infoCmd = require("@lerna/info/command");
const initCmd = require("@lerna/init/command");
const linkCmd = require("@lerna/link/command");
const listCmd = require("@lerna/list/command");
const publishCmd = require("@lerna/publish/command");
const runCmd = require("@lerna/run/command");
const versionCmd = require("@lerna/version/command");
const pkg = require("./package.json");
module.exports = main;
function main(argv) {
const context = {
lernaVersion: pkg.version,
};
return cli()
.command(addCmd)
.command(bootstrapCmd)
.command(changedCmd)
.command(cleanCmd)
.command(createCmd)
.command(diffCmd)
.command(execCmd)
.command(importCmd)
.command(infoCmd)
.command(initCmd)
.command(linkCmd)
.command(listCmd)
.command(publishCmd)
.command(runCmd)
.command(versionCmd)
.parse(argv, context);
}
Commander 類
lerna 的子命令均繼承自 Command 類,比如 lerna init 命令定義爲:
const { Command } = require("@lerna/command");
class InitCommand extends Command {
...
}
Command 類定義在 @lerna/command,位於 lerna/core/command 目錄。
有一些寫法值得借鑑,比如個別方法需要 InitCommand 的實例自行定義,否則拋錯,InitCommand 類的定義如下:
class InitCommand extends Command {
...
initialize() {
throw new ValidationError(this.name, "initialize() needs to be implemented.");
}
execute() {
throw new ValidationError(this.name, "execute() needs to be implemented.");
}
}
import-local
上文提到,lerna/core/lerna/cli.js 描述了命令行的執行入口:
#!/usr/bin/env node
"use strict";
/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
require(".")(process.argv.slice(2));
}
其中,import-local 的作用是,實現本地開發版本和生產版本的切換。import-local/index.js 的內容是:
'use strict';
const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');
module.exports = filename => {
const globalDir = pkgDir.sync(path.dirname(filename));
const relativePath = path.relative(globalDir, filename);
const pkg = require(path.join(globalDir, 'package.json'));
const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
const localNodeModules = path.join(process.cwd(), 'node_modules');
const filenameInLocalNodeModules = !path.relative(localNodeModules, filename).startsWith('..');
// Use path.relative() to detect local package installation,
// because __filename's case is inconsistent on Windows
// Can use === when targeting Node.js 8
// See https://github.com/nodejs/node/issues/6624
return !filenameInLocalNodeModules && localFile && path.relative(localFile, filename) !== '' && require(localFile);
};
依賴管理
Babel 使用 lerna 進行依賴管理。其中,lerna 自己實現了一套依賴管理機制,也支持基於 yarn 的依賴管理。這裏主要介紹 lerna 的 hoisting。
子模塊相同的依賴可以通過依賴提升(hoisting),將相同的依賴安裝在根目錄下,本地包之間用軟連接實現。
lerna bootstrap
該命令執行時,會在每個子項目下面,各自安裝其中 package.json 聲明的依賴。
這樣會有一個問題,相同的依賴會被重複安裝,除了佔用更多空間外,依賴安裝速度也受影響。
lerna bootstrap --hoist
--hoist 標記時,lerna bootstrap 會識別子項目下名稱和版本號相同的依賴,並將其安裝在根目錄的 node_modules 下,子項目的 node_modules 會生成軟連接。
這樣節省了空間,也減少了依賴安裝的耗時。
yarn install
當在項目中聲明 yarn 作爲依賴安裝的底層依賴,如:
lerna.json
{
"npmClient": "yarn",
"useWorkspaces": true,
}
package.json
{
"workspaces": [
"packages/*"
]
}
相對於 lerna,yarn 提供了更強大的依賴分析能力、hoisting 算法。而且,默認情況下,yarn 會開啓 hoist 功能,也可以設置 nohoist 關閉該功能:
{
"workspaces": {
"packages": [
"Packages/*",
],
"nohoist": [
"**"
]
}
}
lerna 中涉及的 git 命令
lerna 中廣泛使用了 git 命令用於內部工作,這裏列舉了 lerna 中使用的 git 命令。
git init
git rev-parse
git describe
git rev-list
git tag
git log
git config
git diff-index
git --version
git show
git am
git reset
git ls-files
git diff-tree
git commit
git ls-remote
git checkout
git push
git add
git remote
git show-ref
沒有必要逐個介紹 git 命令,我們選取幾個不是很常見的 git 命令介紹,瞭解其作用、lerna 哪些命令用到了它們。
git rev-parse
-
主要用於解析 git 引用(如分支名稱、標籤名稱等)或表達式,並輸出對應的 SHA-1 值。它的作用包括但不限於:
-
解析提交、分支、標籤等引用,獲取對應的 SHA-1 值。
-
校驗是否爲有效的引用或表達式。
-
生成 git 對象的唯一標識符。
下面是一個 git rev-parse 命令的執行結果案例:
$ git rev-parse HEAD
f7f6d6f2b6b47eb8c4cf4b8bf5f83e0b8028c031
-
關聯的 lerna 命令
-
lerna version:在執行版本升級操作時,lerna 會使用 git rev-parse 來獲取先前提交的哈希值作爲上一個版本的參考。
-
lerna changed:用於列出自上次標記以來發生變更的包,可能會用到 git rev-parse 來比較不同提交之間的差異。
-
lerna diff:顯示自上次標記以來的所有包的 diff,也可能會使用 git rev-parse 來比較不同提交之間的差異。
-
lerna 源碼案例
libs/commands/import/src/index.ts
getCurrentSHA() {
return this.execSync("git", ["rev-parse", "HEAD"]);
}
getWorkspaceRoot() {
return this.execSync("git", ["rev-parse", "--show-toplevel"]);
}
git describe
-
主要用於根據最接近的標籤來描述當前提交的位置。它的作用包括但不限於:
-
找到最接近當前提交的標籤。
-
根據最接近的標籤以及提交的 SHA-1 值生成一個描述字符串。
-
可以幫助識別當前提交相對於標籤的距離,以及提交是否是基於標籤進行的修改。
下面是一個 git describe 命令的執行結果案例:
$ git describe
polaris-release-1.0.0-c12345
-
關聯的 lerna 命令
-
lerna version:在執行版本升級操作時,lerna 可能會使用 git describe 來確定當前提交的位置,以便生成新的版本號。
-
lerna 源碼案例
libs/commands/diff/src/lib/get-last-commit.ts
export function getLastCommit(execOpts?: ExecOptions) {
if (hasTags(execOpts)) {
log.silly("getLastTagInBranch", "");
return childProcess.execSync("git", ["describe", "--tags", "--abbrev=0"], execOpts);
}
log.silly("getFirstCommit", "");
return childProcess.execSync("git", ["rev-list", "--max-parents=0", "HEAD"], execOpts);
}
git rev-list
-
主要用於列出提交對象的 SHA-1 哈希值。它的作用包括但不限於:
-
列出提交對象的哈希值,可以按時間、作者、提交者等順序進行排序。
-
支持使用範圍、分支、標籤等參數來限制輸出的提交範圍。
下面是一個 git rev-list 命令的執行結果案例:
$ git rev-list HEAD
f7f6d6f2b6b47eb8c4cf4b8bf5f83e0b8028c031
a3d8b4e1c2e1d0a9b8c6e5f7d6a4b3e8a1b2c3d4
這裏 git rev-list HEAD 將列出當前 HEAD 指向的提交及其之前的所有提交的 SHA-1 哈希值。
-
關聯的 lerna 命令
-
lerna changed:列出自上次標記以來發生變更的包,可能會使用 git rev-list 來獲取兩個標記之間的提交列表。
-
lerna diff:顯示自上次標記以來的所有包的 diff,也可能會使用 git rev-list 來獲取兩個標記之間的提交列表。
-
lerna 源碼案例
libs/commands/diff/src/lib/get-last-commit.ts
export function getLastCommit(execOpts?: ExecOptions) {
if (hasTags(execOpts)) {
log.silly("getLastTagInBranch", "");
return childProcess.execSync("git", ["describe", "--tags", "--abbrev=0"], execOpts);
}
log.silly("getFirstCommit", "");
return childProcess.execSync("git", ["rev-list", "--max-parents=0", "HEAD"], execOpts);
}
git diff-index
-
主要用於比較索引和工作樹之間的差異,並將其輸出爲標準輸出。它的作用包括但不限於:
-
檢查暫存區(index)和當前工作目錄之間的差異。
-
可以與不同的選項一起使用,以便輸出不同格式的差異信息。
下面是一個 git diff-index 命令的執行結果案例:
$ git diff-index HEAD
:100644 100644 bcd1234... 0123456... M file.txt
這裏 git diff-index HEAD 將顯示當前提交(HEAD)和工作目錄之間的差異。
-
關聯的 lerna 命令
-
lerna changed:列出自上次標記以來發生變更的包時,可能會用到 git diff-index 來比較索引和工作樹之間的差異。
-
lerna 源碼案例
libs/commands/import/src/index.ts
export class ImportCommand extends Command<ImportCommandOptions> {
...
if (this.execSync("git", ["diff-index", "HEAD"])) {
throw new ValidationError("ECHANGES", "Local repository has un-committed changes");
}
...
}
git diff-tree
-
主要用於比較兩棵樹之間的差異,並以特定的格式輸出。它的作用包括但不限於:
-
比較兩個樹對象之間的差異,例如提交對象和樹對象之間的差異。
-
可以用於查看提交之間的差異,文件的更改等信息。
下面是一個 git diff-tree 命令的執行結果案例:
$ git diff-tree HEAD~2 HEAD
100644 blob a3d8b4e1c2e1d0a9b8c6e5f7d6a4b3e8a1b2c3d4 file.txt
-
關聯的 lerna 命令
-
lerna diff:顯示自上次標記以來的所有包的 diff 時,可能會使用 git diff-tree 來比較不同提交之間的差異。
-
lerna 源碼案例
libs/commands/publish/src/lib/get-projects-with-tagged-packages.ts
export async function getProjectsWithTaggedPackages(
projectNodes: ProjectGraphProjectNodeWithPackage[],
projectFileMap: ProjectFileMap,
execOpts: ExecOptions
): Promise<ProjectGraphProjectNodeWithPackage[]> {
log.silly("getTaggedPackages", "");
// @see https://stackoverflow.com/a/424142/5707
// FIXME: --root is only necessary for tests :P
const result = await childProcess.exec(
"git",
["diff-tree", "--name-only", "--no-commit-id", "--root", "-r", "-c", "HEAD"],
execOpts
);
const stdout: string = result.stdout;
const files = new Set(stdout.split("\n"));
return projectNodes.filter((node) => projectFileMap[node.name]?.some((file) => files.has(file.file)));
}
git show-ref
-
主要用於顯示引用(如分支和標籤)的名稱和其對應的提交哈希值。它的作用包括但不限於:
-
列出 git 倉庫中的所有引用及其對應的提交哈希值。
-
可以用於查看分支、標籤等引用的信息。
下面是一個 git show-ref 命令的執行結果案例:
$ git show-ref
a3d8b4e1c2e1d0a9b8c6e5f7d6a4b3e8a1b2c3d4 HEAD
a3d8b4e1c2e1d0a9b8c6e5f7d6a4b3e8a1b2c3d4 refs/heads/main
f7f6d6f2b6b47eb8c4cf4b8bf5f83e0b8028c031 refs/tags/v1.0.0
-
關聯的 lerna 命令
-
lerna version:在執行版本升級操作時,lerna 可能會使用 git show-ref 來獲取引用的信息,以確定當前提交的位置。
-
lerna 源碼案例
libs/commands/version/src/lib/remote-branch-exists.ts
export function remoteBranchExists(gitRemote: string, branch: string, opts: ExecOptions) {
log.silly("remoteBranchExists", "");
const remoteBranch = `${gitRemote}/${branch}`;
try {
childProcess.execSync("git", ["show-ref", "--verify", `refs/remotes/${remoteBranch}`], opts);
return true;
} catch (e) {
return false;
}
}
四
總結
可以看到,lerna 的內容還是挺多的,如果細緻研究,裏面有大量的方向可以探索,比如文末的 git 命令集,這些值得新開一篇文章詳細介紹,敬請期待!
文 / 效率前端小丙
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/TA-o4acrpQmaX_ej38Yhew