前言:
lerna
做爲一個風靡的前端monorepo
管理工具,在許許多多的大型項目中都獲得的實踐,如今筆者在公司中開發的 monorepo 工具中,monorepo 中子項目的發包能力就是基於 lerna 來完成,所以本篇文章將講解發包中最關鍵的命令即lerna publish
。前端
在上一篇文章中介紹完了 lerna version
的運行機制後,那麼在本篇文章中我將繼續介紹一下 lerna 發包機制中最關鍵的一個 command 即 lerna publish
。node
如今咱們來繼續介紹 lerna publish
運行機制,做爲發包機制中的最後決定性的一個指令,lerna publish 的作的工做其實很簡單,就是將 monorepo 須要發佈的包,發佈到 npm registry 上面去。git
一樣 lerna publish 也分爲幾種不一樣的場景去運行:npm
lerna publish
# lerna version + lerna publish from-git
lerna publish from-git
# 發佈當前 commit 中打上 annoted tag version 的包
lerna publish from-packages
# 發佈 package 中 pkg.json 上的 version 在 registry(高於 latest version)不存在的包
複製代碼
官方文檔,lerna publish
一共有這樣幾種執行表現形式:json
lerna publish 永遠不會發布 package.json 中 private 設置爲 true 的包api
lerna publish
而言)lerna publish from-git
)lerna publish from-package
)lerna publish
自己提供了很多的 options,例如支持發佈測試版本的包即 (lerna version --canary
)。數組
在上文 lerna version 源碼解析中,咱們按照 configureProperties -> initialize -> execute
的順序講解了 lerna version 的執行順序,其實在 lerna 中,幾乎全部子命令源碼的執行順序都是按照這樣一個結構在進行,lerna 自己做爲一個 monorepo,主要是使用 core 核心中的執行機制來去分發命令給各個子項目去執行,所以套路都是同樣的。promise
在開始閱讀以前,我先提供一個總體的思惟導圖,可讓讀者在開始閱讀前有個大體的結構,也便於在閱讀過程能夠藉此來進行回顧:安全
相比較於 lerna version,lerna publish
的這一步就簡單許多,大體就是根據 cli 的 options 對一些參數進行了初始化:bash
configureProperties() {
const {
exact,
gitHead,
gitReset,
tagVersionPrefix = "v",
verifyAccess,
} = this.options;
// 這裏的 requiresGit 指的是除了 from-package 的其它發包方式
if (this.requiresGit && gitHead) {
throw new ValidationError("EGITHEAD", "--git-head is only allowed with 'from-package' positional");
}
// --exact 會指定一個具體的 version 版本,而不會加上 npm 那邊的版本兼容前綴
this.savePrefix = exact ? "" : "^";
// 用於用戶自定義包的版本 tag 前綴,而不是使用默認的 v
this.tagPrefix = tagVersionPrefix;
// --no-git-reset 用於避免 lerna publish 將暫存區的未提交的代碼都 push 到 git 上
this.gitReset = gitReset !== false;
// lerna 發包會默認檢查用戶 npm 權限
// 設置 --no-verify-access 跳過檢查
this.verifyAccess = verifyAccess !== false;
// npm 發包相關配置
this.npmSession = crypto.randomBytes(8).toString("hex");
}
複製代碼
經過註釋就能夠比較清晰的看到一些 options 以及相關參數的初始化,這裏就不詳細介紹。
下面直接進來初始化的流程中來,由於涉及到發包相關的流程,這一步的前面過程涉及到的就是一些關於 npm 相關的 config 初始化,以後再根據不一樣的發包狀況去進行對應的事件註冊,這一步的事件註冊以及執行方式都和 lerna version
源碼解析時比較相似,主要過程能夠分爲三個步驟:
這裏不一樣的發包狀況指的便是在文章開頭介紹的 lerna publish 的幾種執行方式,這裏大體梳理一下如下的步驟:
initialize() {
// --skip-npm 至關於直接執行 lerna version
if (this.options.skipNpm) {
// 該 api 會在下個 major 被棄用
this.logger.warn("deprecated", "Instead of --skip-npm, call `lerna version` directly");
// 這裏咱們能夠看到 lerna 中某個 command 調用其餘 command 都是經過這種鏈式調用的方式
return versionCommand(this.argv).then(() => false);
}
// 1. 初始化 npm config 參數
// session 和 userAgent 都是 npm 發包須要驗證的參數
this.logger.verbose("session", this.npmSession);
this.logger.verbose("user-agent", this.userAgent);
// npm config 相關, 存一些 npm config 相關值
this.conf = npmConf({
lernaCommand: "publish",
_auth: this.options.legacyAuth,
npmSession: this.npmSession,
npmVersion: this.userAgent,
otp: this.options.otp,
registry: this.options.registry,
"ignore-prepublish": this.options.ignorePrepublish,
"ignore-scripts": this.options.ignoreScripts,
});
// --dist-tag 用於 設置發包時候自定義 tag
// 通常默認 tag 是 latest
// lerna 中若是沒指定 --dist-tag, 正式包的 tag 會用 latest, --canary 的測試包會用 canary
const distTag = this.getDistTag();
// 若是該參數存在 會被注入進 npm conf 中
if (distTag) {
this.conf.set("tag", distTag.trim(), "cli");
}
// 註冊運行 lerna.json 裏面的 script 的 runner
this.runPackageLifecycle = createRunner(this.options);
// 若是 lerna 子 package 裏面的 pkg.json 裏面有 pre|post publish 這樣的 script
// 會跳過 lifecycle script 的執行過程,不然會去遞歸執行
this.runRootLifecycle = /^(pre|post)?publish$/.test(process.env.npm_lifecycle_event)
? stage => {
this.logger.warn("lifecycle", "Skipping root %j because it has already been called", stage);
}
: stage => this.runPackageLifecycle(this.project.manifest, stage);
// 2. 根據不一樣的發包狀況執行不一樣的方法
// 經過 promise 構建一個執行鏈, lerna version 裏面講過
let chain = Promise.resolve();
if (this.options.bump === "from-git") {
chain = chain.then(() => this.detectFromGit());
} else if (this.options.bump === "from-package") {
chain = chain.then(() => this.detectFromPackage());
} else if (this.options.canary) {
chain = chain.then(() => this.detectCanaryVersions());
} else {
chain = chain.then(() => versionCommand(this.argv));
}
// 3. 對方法返回的結果作一個處理
return chain.then(result => {
// 若是上一步是走了 lerna version 的 bump version 過程
if (!result) {
return false;
}
// lerna version 返回的結果數組裏面沒有須要更新的 package
if (!result.updates.length) {
this.logger.success("No changed packages to publish");
return false;
}
// publish 的時候把 pkg.json 裏面設置private 爲 false 的包忽略掉
this.updates = result.updates.filter(node => !node.pkg.private);
// 須要更新的包以及對應更新到的version
this.updatesVersions = new Map(result.updatesVersions);
// 再篩選一下須要發包的 packages,根據是否存在 pkg.json
this.packagesToPublish = this.updates.map(node => node.pkg);
// 用於發佈 lerna 管理的 packages 的一些子目錄例如 dist
// 參考 --contents 這個 options
if (this.options.contents) {
// 把這些目錄寫進須要發包的 pkg.json 中
for (const pkg of this.packagesToPublish) {
pkg.contents = this.options.contents;
}
}
// 用於確認上面除了 versioncommand 的其餘三種執行狀況
// 例如 versionCommand 有本身的 confirm 過程
if (result.needsConfirmation) {
return this.confirmPublish();
}
return true;
});
}
複製代碼
initialize
前面有介紹主要分爲三個步驟來執行,所以 一、3 兩個步驟根據註釋來理解過程仍是比較清晰的,這裏主要介紹一下第二步即 根據不一樣的發包狀況來執行不用的方法,具體代碼:
if (this.options.bump === "from-git") {
chain = chain.then(() => this.detectFromGit());
} else if (this.options.bump === "from-package") {
chain = chain.then(() => this.detectFromPackage());
} else if (this.options.canary) {
chain = chain.then(() => this.detectCanaryVersions());
} else {
chain = chain.then(() => versionCommand(this.argv));
}
複製代碼
首先根據上面代碼中以及文章開頭介紹,能夠很清晰的知道具體分爲這幾種狀況:
from-git
即根據 git commit
上的 annotaed tag
進行發包from-package
即根據 lerna 下的 package 裏面的 pkg.json 的 version 變更來發包--canary
發測試版本的包下面從這幾種狀況作個介紹:
from-git
這一步的執行入口函數是 detectFromGit
,咱們直接看這個函數的執行過程:
detectFromGit() {
const matchingPattern = this.project.isIndependent() ? "*@*" : `${this.tagPrefix}*.*.*`;
let chain = Promise.resolve();
// 1. 驗證當前的 git 工做區域是否乾淨 經過 git describe 來找
chain = chain.then(() => this.verifyWorkingTreeClean());
// 2. 拿到當前 commit 上面的 tag
chain = chain.then(() => getCurrentTags(this.execOpts, matchingPattern));
// 3. 經過上一步的 tag 拿到須要更新的 pkg 的數組
chain = chain.then(taggedPackageNames => {
if (!taggedPackageNames.length) {
this.logger.notice("from-git", "No tagged release found");
return [];
}
// 獨立發包模式就拿到全部包的數組
if (this.project.isIndependent()) {
return taggedPackageNames.map(name => this.packageGraph.get(name));
}
// 固定模式只用拿到一個版本,全部的包都用一個版本
return getTaggedPackages(this.packageGraph, this.project.rootPath, this.execOpts);
});
// 4. 清除掉更新 packages 裏面 pkg.json 中設置了 private 爲 false 的包
chain = chain.then(updates => updates.filter(node => !node.pkg.private));
// 5. updateVersions 存須要發佈的包名以及發佈的版本
return chain.then(updates => {
const updatesVersions = updates.map(node => [node.name, node.version]);
return {
updates,
updatesVersions,
needsConfirmation: true,
};
});
}
複製代碼
能夠看到這一步函數的執行過程仍是比較簡單明瞭的,在上面註釋中根據不一樣的方法執行過程分爲了 5 個步驟。主要就是根據當前 commit 拿到 tags 裏面的 packages 而後返回這些 packages 以及其版本信息。
from-package
這一步執行的入口函數是 detectFromPackage
,直接看執行過程:
detectFromPackage() {
let chain = Promise.resolve();
// 1. 驗證當前 git 工做區是否乾淨,步驟同上
chain = chain.then(() => this.verifyWorkingTreeClean());
// 2. 經過 getUnpublishedPackages 篩除 private 爲 true 的 package && 拿到須要發佈的pkg
// 這一步用了 npm config 裏面的快照來作對比
chain = chain.then(() => getUnpublishedPackages(this.packageGraph, this.conf.snapshot));
// 3. 驗證結果符合預期否
chain = chain.then(unpublished => {
if (!unpublished.length) {
this.logger.notice("from-package", "No unpublished release found");
}
return unpublished;
});
// 4. updateVersions 存須要發佈的包名以及發佈的版本,返回結果
return chain.then(updates => {
const updatesVersions = updates.map(node => [node.name, node.version]);
return {
updates,
updatesVersions,
needsConfirmation: true,
};
});
}
複製代碼
這一步主要是在 getUnpublishedPackages
這一步篩選出須要更新的 packages,這裏 lerna 做者使用了本身封裝的 pacote 庫來去作一些關於版本的比對,從而獲得須要更新的 packages,這裏有想了解的能夠自行去閱讀一下,不作過多贅述。
--canary
這一步執行的入口函數是 detectCanaryVersions
,直接看執行過程:
detectCanaryVersions() {
// 初始化處理參數
const { cwd } = this.execOpts;
const {
bump = "prepatch",
preid = "alpha",
ignoreChanges,
forcePublish,
includeMergedTags,
} = this.options;
const release = bump.startsWith("pre") ? bump.replace("release", "patch") : `pre${bump}`;
let chain = Promise.resolve();
// 1. 驗證當前 git 區是否乾淨
chain = chain.then(() => this.verifyWorkingTreeClean());
// 2. 找到自上次來修改過的 packages 同時篩掉 private 爲 false 的 pkg
chain = chain.then(() =>
collectUpdates(this.packageGraph.rawPackageList, this.packageGraph, this.execOpts, {
bump: "prerelease",
canary: true,
ignoreChanges,
forcePublish,
includeMergedTags,
}).filter(node => !node.pkg.private)
);
const makeVersion = fallback => ({ lastVersion = fallback, refCount, sha }) => {
// --canary 會經過上一次的 version 來計算出此次的 version
const nextVersion = semver.inc(lastVersion.replace(this.tagPrefix, ""), release.replace("pre", ""));
return `${nextVersion}-${preid}.${Math.max(0, refCount - 1)}+${sha}`;
};
// 3. 根據不一樣的 mode 計算出包及版本相關參數
if (this.project.isIndependent()) {
// 獨立發包模式
chain = chain.then(updates =>
// pMap 是個鏈式執行過程,上一步的結果會給到下一步
pMap(updates, node =>
// 根據 tag 匹配出須要發佈的包
describeRef(
{
match: `${node.name}@*`,
cwd,
},
includeMergedTags
)
// 經過上面的 makeVersion 方法來計算髮布的 canary 版本
.then(makeVersion(node.version))
// 返回出去,這裏實際上就是個 updateVerions 數組
.then(version => [node.name, version])
).then(updatesVersions => ({
updates,
updatesVersions,
}))
);
} else {
// 固定的模式,那麼全部的包都會使用一個版本(lerna.json 裏面的版本)
chain = chain.then(updates =>
describeRef(
{
match: `${this.tagPrefix}*.*.*`,
cwd,
},
includeMergedTags
)
// 只用一個 version 去進行計算
.then(makeVersion(this.project.version))
.then(version => updates.map(node => [node.name, version]))
.then(updatesVersions => ({
updates,
updatesVersions,
}))
);
}
// 4. 返回結果
return chain.then(({ updates, updatesVersions }) => ({
updates,
updatesVersions,
needsConfirmation: true,
}));
}
複製代碼
相比較於上面兩步, --canary
的處理過程或許看上去要複雜一些,其實否則,根據上面代碼註釋中的內容能夠比較清晰的看到整個執行流程,不過多了幾種特殊狀況須要去作一些判斷,其中比較複雜的第三步,是須要經過 tag 獲得一些相關的信息,須要更新的包,而後針對這些包現有的版本去作一些計算,能夠參考上面的 makeVersion
方法,這裏就根據 lerna 的 mode 分爲了兩種狀況。
其中這裏第二步還用到了在 lerna version 中收集變動的包的方法:collectUpdates
。具體的執行機制能夠參考個人上一篇關於 lerna version 的文章。
若是不帶參數的話,那麼這一步就會直接執行一個 lerna version 的過程,通常 lerna publish 的預期行爲是這樣:
chain = chain.then(() => versionCommand(this.argv));
複製代碼
lerna version 的具體執行機制能夠參考個人上一篇文章。
看完這幾種狀況以後再回到開頭,再回顧一下 initialize 這一步最後對結果的一個處理過程,大體 initialize 的一個流程就這樣結束了。
最後總結一下 lerna publish 的初始化過程,主要就是根據不一樣的發包狀況,而後計算出須要發佈的包的信息,例如包名稱和更新版本。用於下一步發包的 execute 作準備。
lerna publish
的最後一步即發包的過程就是在這裏完成,代碼結構爲:
execute() {
let chain = Promise.resolve();
// 1. 驗證 npm 源、權限,項目的 License 以內
chain = chain.then(() => this.prepareRegistryActions());
chain = chain.then(() => this.prepareLicenseActions());
if (this.options.canary) {
// 若是是測試包,更新到測試包的 version
chain = chain.then(() => this.updateCanaryVersions());
}
// 2. 更新本地依賴包版本 && gitHead
chain = chain.then(() => this.resolveLocalDependencyLinks());
chain = chain.then(() => this.annotateGitHead());
// 3. 更新寫入本地
chain = chain.then(() => this.serializeChanges());
// 4. 對 package pack
chain = chain.then(() => this.packUpdated());
// 5. 發佈包
chain = chain.then(() => this.publishPacked());
if (this.gitReset) {
// 設置了 --no-git-reset 會把 working tree 的版本修改重置
// lerna 每次發包都會把更新的 package.json 的 version 的修改提交到 git 上去
// 若是發測試包,這多是不必,所以能夠用這個選項把修改 reset 掉
chain = chain.then(() => this.resetChanges());
}
// 作後續的處理
return chain.then(() => {
// 發佈包的數量
const count = this.packagesToPublish.length;
// 發佈包的名稱以及版本,用於輸出展現
const message = this.packagesToPublish.map(pkg => ` - ${pkg.name}@${pkg.version}`);
output("Successfully published:");
output(message.join(os.EOL));
this.logger.success("published", "%d %s", count, count === 1 ? "package" : "packages");
});
}
複製代碼
execute
是 lerna publish
的主要部分了,這一步的相對而言信息量比較巨大,我接下來會將上面的步驟拆一拆,一步一步來說解 execute
這一步是怎麼完成 lerna 發包的整個過程的。
首先能夠看到上面代碼中,我經過註釋將這個步驟分紅了六步:
首先上面能夠看到,這一步分爲兩個方法,一步是作 npm 相關的驗證:prepareRegistryActions
prepareRegistryActions() {
let chain = Promise.resolve();
if (this.conf.get("registry") !== "https://registry.npmjs.org/") {
// 這裏的 registry 若是 url 是三方的例如公司的源,這裏會跳事後面的檢查
return chain;
}
// --no-verify-access 中止校驗,默認會校驗
if (this.verifyAccess) {
// 拿用戶的 npm username,拿不到在 getNpmUsername 會拋錯
chain = chain.then(() => getNpmUsername(this.conf.snapshot));
// 根據 username 對要發佈的包作個鑑權
chain = chain.then(username => {
if (username) {
return verifyNpmPackageAccess(this.packagesToPublish, username, this.conf.snapshot);
}
});
// 校驗用戶是否需進行 2fa 的驗證 -- 安全驗證相關
chain = chain.then(() => getTwoFactorAuthRequired(this.conf.snapshot));
chain = chain.then(isRequired => {
// 記錄一下
this.twoFactorAuthRequired = isRequired;
});
}
return chain;
}
複製代碼
prepareRegistryActions
執行時會先去校驗 registry,若是是第三方的 registry,會中止校驗,用戶在發包設置了 no-verify-access
就不進行後面校驗,默認會校驗。
校驗過程是首先經過 getNpmUsername
去拿到用戶的 username,這裏是經過 npm 提供的相關接口來獲取,具體流程能夠自行參考。拿到 username 以後根據 username 以及本次 publish 中須要發佈的包的信息去作一個鑑權,判斷用戶是否用該包的讀寫發包權限,沒有就會拋錯,最後一步是個 2fa 的驗證,通常 npm 包都不會開啓,主要是爲了安全做用作二次驗證使用,這裏不作具體講解。
下面在看 license 的校驗過程,方法是 prepareLicenseActions
:
prepareLicenseActions() {
return Promise.resolve()
// 經過 glob 的方式去找到待發布的包中沒有 licenses 的
.then(() => getPackagesWithoutLicense(this.project, this.packagesToPublish))
.then(packagesWithoutLicense => {
// 對於沒有 liecense 的包會打個 warnning 出來
if (packagesWithoutLicense.length && !this.project.licensePath) {
this.packagesToBeLicensed = [];
const names = packagesWithoutLicense.map(pkg => pkg.name);
const noun = names.length > 1 ? "Packages" : "Package";
const verb = names.length > 1 ? "are" : "is";
const list =
names.length > 1
? `${names.slice(0, -1).join(", ")}${names.length > 2 ? "," : ""} and ${ names[names.length - 1] /* oxford commas _are_ that important */ }`
: names[0];
this.logger.warn(
"ENOLICENSE",
"%s %s %s missing a license.\n%s\n%s",
noun,
list,
verb,
"One way to fix this is to add a LICENSE.md file to the root of this repository.",
"See https://choosealicense.com for additional guidance."
);
} else {
// 記錄一下
this.packagesToBeLicensed = packagesWithoutLicense;
}
});
}
複製代碼
這一步並不會對主要流程有什麼影響,主要就是找目前待發布的包中沒有 license 的,而後給個 warnning 提示,這裏找的方式使用過 lerna 本身構造的 project graph 去篩待發布包中不存在 liecense 文件的路徑,想了解具體過程參考 getPackagesWithoutLicense
。
能夠你會對更新本地依賴版本這一步可能會有些迷惑,這裏舉個例子來解釋一下,在 lerna 中,若是 workspaces 以前存在依賴的話,在此次發包中,例如 A 這個包依賴了 B,B 在此次發包中版本升級了,那麼這裏 A 裏面依賴的 B 也要更新到對應的版本。
來看一下這一步:
resolveLocalDependencyLinks() {
// 先找到依賴過本地包的包
// lerna 中 A, B 都是 workspace, A 依賴 B 引入的時候是經過 symlink 引入的
// 所以這裏找 B 的依賴包只用判斷 A 這裏 resolved 的是否是個目錄就行
const updatesWithLocalLinks = this.updates.filter(node =>
Array.from(node.localDependencies.values()).some(resolved => resolved.type === "directory")
);
// 拿到上一步結果以後,就把對應的更新寫入 A
return pMap(updatesWithLocalLinks, node => {
for (const [depName, resolved] of node.localDependencies) {
// 注意這裏 lerna 是不會處理 B: ../../file/xxx 這種引入狀況的
const depVersion = this.updatesVersions.get(depName) || this.packageGraph.get(depName).pkg.version;
// 以 A 爲例子,這裏 A 的 pkg.json 中的B 就要更新到發包的版本
node.pkg.updateLocalDependency(resolved, depVersion, this.savePrefix);
}
});
}
複製代碼
這裏涉及到的一些操做方法,都是來自於 lerna 構建的 project graph,這部分能夠去參考一下 lerna core 中源碼。
這裏的 gitHead 是一個 hash 值,用戶能夠經過 --git-head 來自行指定,若是不指定的話,lerna 這裏會默認幫你取當前 commit 的 hash 值,即經過 git rev-parse HEAD
來獲取,通常 gitHead 結合 from-package
來使用,先看看代碼:
annotateGitHead() {
try {
// 用戶若是沒有默認指定就使用最近的 commit hash 值
// getCurrentSHA 就是執行了一次 git rev-parse HEAD
const gitHead = this.options.gitHead || getCurrentSHA(this.execOpts);
for (const pkg of this.packagesToPublish) {
// gitHead 是用來關聯 package 和 git 記錄
// npm publish 正常狀況下須要該字段
pkg.set("gitHead", gitHead);
}
} catch (err) {
}
}
複製代碼
在使用 from-package
的方式進行發包的時候,會把這個 githead 字段寫在 package.json
裏面。
這一步就是將第二步的一些更新直接寫到 lerna 中對應項目裏面去,即寫到磁盤裏面,主要的方法爲:
serializeChanges() {
return pMap(this.packagesToPublish, pkg => pkg.serialize());
}
複製代碼
這個 pkg.serialize()
方法,是能夠在 lerna 的 core 中找到的,主要做用就是將相關的更新寫入本地磁盤:
serialize() {
// 這裏的 writePkg 封裝了 write-package-json 這個方法
return writePkg(this.manifestLocation, this[PKG]).then(() => this);
}
複製代碼
在講解以前,咱們得先知道 npm pack
這個操做是幹什麼的,它會打包當前的文件夾內容打包成一個 tar 包,咱們在執行 npm publish 的時候會常常看到這個操做:
不過 npm publish 幫咱們封裝了這個過程,lerna publish 中也會有這個過程,這已是發包前的最後一個操做了,具體可參考代碼:
packUpdated() {
let chain = Promise.resolve();
// ...
const opts = this.conf.snapshot;
const mapper = pPipe(
[
// packDirectory 會給對應 pkg 的文件夾打個 tar 包出來
// 相似於上面的 npm pack
pkg =>
pulseTillDone(packDirectory(pkg, pkg.location, opts)).then(packed => {
pkg.packed = packed;
return pkg.refresh();
}),
].filter(Boolean)
);
// 這裏會按照拓撲序列去對要發佈的包進行 pack
chain = chain.then(() => this.topoMapPackages(mapper));
return pFinally(chain, () => tracker.finish());
}
複製代碼
這一步首先能夠參考 topoMapPackages
這個方法,他會按照拓撲順序去對須要更新的包進行 pack,這裏 publish 由於涉及到包之間的一些依賴關係,所以只能按照拓撲的順序去執行,this.packagesToPublish
裏面存的是待發布的包:
topoMapPackages(mapper) {
// 這裏是做者的一個註釋:
// we don't respect --no-sort here, sorry
return runTopologically(this.packagesToPublish, mapper, {
concurrency: this.concurrency,
rejectCycles: this.options.rejectCycles,
graphType: this.options.graphType === "all" ? "allDependencies" : "dependencies",
});
}
複製代碼
所以這裏會按照拓撲順序去對要發佈的包進行打包成 tar 包的操做,具體執行方法是 packDirectory
這個方法,這個方法我只貼一下打包的那一段邏輯,還有一些其餘的預處理邏輯作了一下刪除:
const tar = require("tar");
const packlist = require("npm-packlist");
function packDirectory(_pkg, dir, _opts) {
// ...
let chain = Promise.resolve();
// 拿到待發布 pkg 的 目錄信息
chain = chain.then(() => packlist({ path: pkg.contents }));
// 對目錄下面的一些文件夾打包
chain = chain.then(files =>
// 具體參數去參考 tar 這個 npm 包
tar.create(
{
cwd: pkg.contents,
prefix: "package/",
portable: true,
mtime: new Date("1985-10-26T08:15:00.000Z"),
gzip: true,
},
files.map(f => `./${f}`)
)
);
// 將文件處理成 stream 形式寫到一個臨時目錄下面
// 發佈完了會刪除
chain = chain.then(stream => tempWrite(stream, getTarballName(pkg)));
chain = chain.then(tarFilePath =>
getPacked(pkg, tarFilePath).then(packed =>
Promise.resolve()
.then(() => runLifecycle(pkg, "postpack", opts))
.then(() => packed)
)
);
return chain;
}
複製代碼
在上一步完成了待發布包的打包操做以後,這一步就是 lerna publish 整個流程的最後一步了!
這一步會將上一次打包的內容直接發佈出去,先來看一下代碼:
publishPacked() {
let chain = Promise.resolve();
// 前面說過的 2fa 2次驗證,這裏會驗證一下
if (this.twoFactorAuthRequired) {
chain = chain.then(() => this.requestOneTimePassword());
}
const opts = Object.assign(this.conf.snapshot, {
// 設置了 tempTag 就先用 lerna-temp
tag: this.options.tempTag ? "lerna-temp" : this.conf.get("tag"),
});
const mapper = pPipe(
[
pkg => {
// 拿到 pkg 上一次發佈的 tag,經過 semver.prerelease 進行判斷
const preDistTag = this.getPreDistTag(pkg);
// 取一下 tag,通常這裏會取 opts.tag,針對於每一個包的狀況不一樣
const tag = !this.options.tempTag && preDistTag ? preDistTag : opts.tag;
// 這裏 rewrite 一下 tag
const pkgOpts = Object.assign({}, opts, { tag });
// 發佈包這個操做經過 npmPublish 這個過程來完成
return pulseTillDone(npmPublish(pkg, pkg.packed.tarFilePath, pkgOpts, this.otpCache)).then(() => {
return pkg;
});
}
].filter(Boolean)
);
// 這裏和上一步 pack 同樣,按照拓撲執行
chain = chain.then(() => this.topoMapPackages(mapper));
return pFinally(chain, () => tracker.finish());
}
複製代碼
上一步講了 topoMapPackages
這個方法,這裏一樣的,它會按照拓撲順序去發佈待發布的 pkg。
在 npmPublish
這個方法中,會將前面打包的 pkg 的 tar 包 publish 到 npm 上面去,這裏用的是 lerna 做者本身的一個包,感興趣的能夠去 npm 上搜一下:@evocateur/libnpmpublish
這個包能夠不用擔憂 tarball 打包自於哪一個 pkg,只要你有個 tarball 它會幫你直接上傳到 npm 上面去來完成一次發佈,具體的內容能夠在 npm 中找到。
這裏由於引入了一些外部包加上這裏有太多的邊界條件處理,這裏就不具體去看 npmPublish
這個方法了,貼上發佈的那部分代碼,能夠參考一下:
const { publish } = require("@evocateur/libnpmpublish");
return otplease(innerOpts => publish(manifest, tarData, innerOpts), opts, otpCache).catch(err => {
// re-throw to break chain upstream
throw err;
});
複製代碼
那麼再走到這一步結束以後,基本上整個 lerna 的發包流程都走完了。
後續的一些收尾工做的處理,能夠再拉回 執行(execute) 這一節開頭的代碼分析那裏。
本文從源碼角度剖析了一下 lerna publish 的執行機制,對於一些邊界的 corner case 有些刪減,按照主線講解了 lerna publish 是怎麼完成 lerna monorepo 中的整個發包流程操做的。但願本系列的文章能對你有所幫助。