<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ā)包機制 —— lerna publish

          共 22986字,需瀏覽 46分鐘

           ·

          2021-04-06 09:19

          點擊上方藍字“TianTianUp”關(guān)注我
          您的關(guān)注意義重大

          前言:lerna 作為一個風靡的前端 monorepo 管理工具,在許許多多的大型項目中都得到的實踐,現(xiàn)在筆者在公司中開發(fā)的 monorepo 工具中,monorepo 中子項目的發(fā)包能力就是基于 lerna 來完成,因此本篇文章將講解發(fā)包中最關(guān)鍵的命令即 lerna publish。

          在上一篇文章中介紹完了 lerna version 的運行機制后,那么在本篇文章中我將繼續(xù)介紹一下 lerna 發(fā)包機制中最關(guān)鍵的一個 command 即 lerna publish。

          現(xiàn)在我們來繼續(xù)介紹 lerna publish 運行機制,作為發(fā)包機制中的最后決定性的一個指令,lerna publish 的做的工作其實很簡單,就是將 monorepo 需要發(fā)布的包,發(fā)布到 npm registry 上面去。

          同樣 lerna publish 也分為幾種不同的場景去運行:

          lerna publish  
          # lerna version + lerna publish from-git
          lerna publish from-git
          # 發(fā)布當前 commit 中打上 annoted tag version 的包
          lerna publish from-packages
          # 發(fā)布 package 中 pkg.json 上的 version 在 registry(高于 latest version)不存在的包

          官方文檔,lerna publish 一共有這樣幾種執(zhí)行表現(xiàn)形式:

          lerna publish 永遠不會發(fā)布 package.json 中 private 設(shè)置為 true 的包

          • 發(fā)布自上次發(fā)布來有更新的包(這里的上次發(fā)布也是基于上次執(zhí)行lerna publish 而言)

          • 發(fā)布在當前 commit 上打上了 annotated tag 的包(即 lerna publish from-git)

          • 發(fā)布在最近 commit 中修改了 package.json 中的 version (且該 version 在 registry 中沒有發(fā)布過)的包(即 lerna publish from-package)

          • 發(fā)布在上一次提交中更新了的 unversioned 的測試版本的包(以及依賴了的包)

          lerna publish 本身提供了不少的 options,例如支持發(fā)布測試版本的包即 (lerna version --canary)。

          在上文 lerna version 源碼解析中,我們按照 configureProperties -> initialize -> execute 的順序講解了 lerna version 的執(zhí)行順序,其實在 lerna 中,幾乎所有子命令源碼的執(zhí)行順序都是按照這樣一個結(jié)構(gòu)在進行,lerna 本身作為一個 monorepo,主要是使用 core 核心中的執(zhí)行機制來去分發(fā)命令給各個子項目去執(zhí)行,因此套路都是一樣的。

          在開始閱讀之前,我先提供一個整體的思維導圖,可以讓讀者在開始閱讀前有個大致的結(jié)構(gòu),也便于在閱讀過程可以借此來進行回顧:

          設(shè)置屬性(configureProperties)

          相比較于 lerna version,lerna publish 的這一步就簡單許多,大致就是根據(jù) cli 的 options 對一些參數(shù)進行了初始化:

          configureProperties() {
          const {
          exact,
          gitHead,
          gitReset,
          tagVersionPrefix = "v",
          verifyAccess,
          } = this.options;

          // 這里的 requiresGit 指的是除了 from-package 的其它發(fā)包方式
          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 將暫存區(qū)的未提交的代碼都 push 到 git 上
          this.gitReset = gitReset !== false;

          // lerna 發(fā)包會默認檢查用戶 npm 權(quán)限
          // 設(shè)置 --no-verify-access 跳過檢查
          this.verifyAccess = verifyAccess !== false;

          // npm 發(fā)包相關(guān)配置
          this.npmSession = crypto.randomBytes(8).toString("hex");
          }

          通過注釋就可以比較清晰的看到一些 options 以及相關(guān)參數(shù)的初始化,這里就不詳細介紹。

          初始化(initialize)

          下面直接進來初始化的流程中來,因為涉及到發(fā)包相關(guān)的流程,這一步的前面過程涉及到的就是一些關(guān)于 npm 相關(guān)的 config 初始化,之后再根據(jù)不同的發(fā)包情況去進行對應(yīng)的事件注冊,這一步的事件注冊以及執(zhí)行方式都和 lerna version 源碼解析時比較類似,主要過程可以分為三個步驟:

          1. 初始化 npm config 參數(shù)

          2. 根據(jù)不同的發(fā)包情況執(zhí)行不同的方法

          3. 處理上一步返回的結(jié)果

          這里不同的發(fā)包情況指的即是在文章開頭介紹的 lerna publish 的幾種執(zhí)行方式,這里大致梳理一下以下的步驟:

            initialize() {
          // --skip-npm 相當于直接執(zhí)行 lerna version
          if (this.options.skipNpm) {
          // 該 api 會在下個 major 被棄用
          this.logger.warn("deprecated", "Instead of --skip-npm, call `lerna version` directly");
          // 這里我們可以看到 lerna 中某個 command 調(diào)用其他 command 都是通過這種鏈式調(diào)用的方式
          return versionCommand(this.argv).then(() => false);
          }

          // 1. 初始化 npm config 參數(shù)

          // session 和 userAgent 都是 npm 發(fā)包需要驗證的參數(shù)
          this.logger.verbose("session", this.npmSession);
          this.logger.verbose("user-agent", this.userAgent);

          // npm config 相關(guān), 存一些 npm config 相關(guān)值
          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 用于 設(shè)置發(fā)包時候自定義 tag
          // 一般默認 tag 是 latest
          // lerna 中如果沒指定 --dist-tag, 正式包的 tag 會用 latest, --canary 的測試包會用 canary
          const distTag = this.getDistTag();

          // 如果該參數(shù)存在 會被注入進 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 的執(zhí)行過程,否則會去遞歸執(zhí)行
          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. 根據(jù)不同的發(fā)包情況執(zhí)行不同的方法

          // 通過 promise 構(gòu)建一個執(zhí)行鏈, 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. 對方法返回的結(jié)果做一個處理

          return chain.then(result => {
          // 如果上一步是走了 lerna version 的 bump version 過程
          if (!result) {
          return false;
          }

          // lerna version 返回的結(jié)果數(shù)組里面沒有需要更新的 package
          if (!result.updates.length) {
          this.logger.success("No changed packages to publish");
          return false;
          }

          // publish 的時候把 pkg.json 里面設(shè)置private 為 false 的包忽略掉
          this.updates = result.updates.filter(node => !node.pkg.private);
          // 需要更新的包以及對應(yīng)更新到的version
          this.updatesVersions = new Map(result.updatesVersions);

          // 再篩選一下需要發(fā)包的 packages,根據(jù)是否存在 pkg.json
          this.packagesToPublish = this.updates.map(node => node.pkg);

          // 用于發(fā)布 lerna 管理的 packages 的一些子目錄例如 dist
          // 參考 --contents 這個 options
          if (this.options.contents) {
          // 把這些目錄寫進需要發(fā)包的 pkg.json 中
          for (const pkg of this.packagesToPublish) {
          pkg.contents = this.options.contents;
          }
          }

          // 用于確認上面除了 versioncommand 的其他三種執(zhí)行情況
          // 例如 versionCommand 有自己的 confirm 過程
          if (result.needsConfirmation) {
          return this.confirmPublish();
          }

          return true;
          });
          }

          initialize 前面有介紹主要分為三個步驟來執(zhí)行,因此 1、3 兩個步驟根據(jù)注釋來理解過程還是比較清晰的,這里主要介紹一下第二步即 根據(jù)不同的發(fā)包情況來執(zhí)行不用的方法,具體代碼:

          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));
          }

          首先根據(jù)上面代碼中以及文章開頭介紹,可以很清晰的知道具體分為這幾種情況:

          • from-git 即根據(jù) git commit 上的 annotaed tag 進行發(fā)包

          • from-package 即根據(jù) lerna 下的 package 里面的 pkg.json 的 version 變動來發(fā)包

          • --canary 發(fā)測試版本的包

          • 剩下不帶參數(shù)的情況就直接走一個 bump version(即執(zhí)行 lerna version)

          下面從這幾種情況做個介紹:

          from-git

          這一步的執(zhí)行入口函數(shù)是 detectFromGit ,我們直接看這個函數(shù)的執(zhí)行過程:

          detectFromGit() {

          const matchingPattern = this.project.isIndependent() ? "*@*" : `${this.tagPrefix}*.*.*`;

          let chain = Promise.resolve();

          // 1. 驗證當前的 git 工作區(qū)域是否干凈 通過 git describe 來找
          chain = chain.then(() => this.verifyWorkingTreeClean());

          // 2. 拿到當前 commit 上面的 tag
          chain = chain.then(() => getCurrentTags(this.execOpts, matchingPattern));

          // 3. 通過上一步的 tag 拿到需要更新的 pkg 的數(shù)組
          chain = chain.then(taggedPackageNames => {
          if (!taggedPackageNames.length) {
          this.logger.notice("from-git", "No tagged release found");

          return [];
          }
          // 獨立發(fā)包模式就拿到所有包的數(shù)組
          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 中設(shè)置了 private 為 false 的包
          chain = chain.then(updates => updates.filter(node => !node.pkg.private));

          // 5. updateVersions 存需要發(fā)布的包名以及發(fā)布的版本
          return chain.then(updates => {
          const updatesVersions = updates.map(node => [node.name, node.version]);

          return {
          updates,
          updatesVersions,
          needsConfirmation: true,
          };
          });
          }

          可以看到這一步函數(shù)的執(zhí)行過程還是比較簡單明了的,在上面注釋中根據(jù)不同的方法執(zhí)行過程分為了 5 個步驟。主要就是根據(jù)當前 commit 拿到 tags 里面的 packages 然后返回這些 packages 以及其版本信息。

          from-package

          這一步執(zhí)行的入口函數(shù)是 detectFromPackage ,直接看執(zhí)行過程:

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

          // 1. 驗證當前 git 工作區(qū)是否干凈,步驟同上
          chain = chain.then(() => this.verifyWorkingTreeClean());

          // 2. 通過 getUnpublishedPackages 篩除 private 為 true 的 package && 拿到需要發(fā)布的pkg
          // 這一步用了 npm config 里面的快照來做對比
          chain = chain.then(() => getUnpublishedPackages(this.packageGraph, this.conf.snapshot));

          // 3. 驗證結(jié)果符合預期否
          chain = chain.then(unpublished => {
          if (!unpublished.length) {
          this.logger.notice("from-package", "No unpublished release found");
          }

          return unpublished;
          });

          // 4. updateVersions 存需要發(fā)布的包名以及發(fā)布的版本,返回結(jié)果
          return chain.then(updates => {
          const updatesVersions = updates.map(node => [node.name, node.version]);

          return {
          updates,
          updatesVersions,
          needsConfirmation: true,
          };
          });
          }

          這一步主要是在 getUnpublishedPackages 這一步篩選出需要更新的 packages,這里 lerna 作者使用了自己封裝的 pacote 庫來去做一些關(guān)于版本的比對,從而得到需要更新的 packages,這里有想了解的可以自行去閱讀一下,不做過多贅述。

          --canary

          這一步執(zhí)行的入口函數(shù)是 detectCanaryVersions ,直接看執(zhí)行過程:

          detectCanaryVersions() {
          // 初始化處理參數(shù)
          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 區(qū)是否干凈
          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. 根據(jù)不同的 mode 計算出包及版本相關(guān)參數(shù)
          if (this.project.isIndependent()) {
          // 獨立發(fā)包模式
          chain = chain.then(updates =>
          // pMap 是個鏈式執(zhí)行過程,上一步的結(jié)果會給到下一步
          pMap(updates, node =>
          // 根據(jù) tag 匹配出需要發(fā)布的包
          describeRef(
          {
          match: `${node.name}@*`,
          cwd,
          },
          includeMergedTags
          )
          // 通過上面的 makeVersion 方法來計算發(fā)布的 canary 版本
          .then(makeVersion(node.version))
          // 返回出去,這里實際上就是個 updateVerions 數(shù)組
          .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. 返回結(jié)果
          return chain.then(({ updates, updatesVersions }) => ({
          updates,
          updatesVersions,
          needsConfirmation: true,
          }));
          }

          相比較于上面兩步, --canary 的處理過程或許看上去要復雜一些,其實不然,根據(jù)上面代碼注釋中的內(nèi)容可以比較清晰的看到整個執(zhí)行流程,不過多了幾種特殊情況需要去做一些判斷,其中比較復雜的第三步,是需要通過 tag 得到一些相關(guān)的信息,需要更新的包,然后針對這些包現(xiàn)有的版本去做一些計算,可以參考上面的 makeVersion 方法,這里就根據(jù) lerna 的 mode 分為了兩種情況。

          其中這里第二步還用到了在 lerna version 中收集變更的包的方法:collectUpdates。具體的執(zhí)行機制可以參考我的上一篇關(guān)于 lerna version 的文章。

          bump version

          如果不帶參數(shù)的話,那么這一步就會直接執(zhí)行一個 lerna version 的過程,一般 lerna publish 的預期行為是這樣:

          chain = chain.then(() => versionCommand(this.argv));

          lerna version 的具體執(zhí)行機制可以參考我的上一篇文章。

          看完這幾種情況之后再回到開頭,再回顧一下 initialize 這一步最后對結(jié)果的一個處理過程,大致 initialize 的一個流程就這樣結(jié)束了。

          最后總結(jié)一下 lerna publish 的初始化過程,主要就是根據(jù)不同的發(fā)包情況,然后計算出需要發(fā)布的包的信息,例如包名稱和更新版本。用于下一步發(fā)包的 execute 做準備。

          執(zhí)行(execute)

          lerna publish 的最后一步即發(fā)包的過程就是在這里完成,代碼結(jié)構(gòu)為:

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

          // 1. 驗證 npm 源、權(quán)限,項目的 License 之內(nèi)
          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. 發(fā)布包
          chain = chain.then(() => this.publishPacked());

          if (this.gitReset) {
          // 設(shè)置了 --no-git-reset 會把 working tree 的版本修改重置
          // lerna 每次發(fā)包都會把更新的 package.json 的 version 的修改提交到 git 上去
          // 如果發(fā)測試包,這可能是沒必要,因此可以用這個選項把修改 reset 掉
          chain = chain.then(() => this.resetChanges());
          }

          // 做后續(xù)的處理
          return chain.then(() => {
          // 發(fā)布包的數(shù)量
          const count = this.packagesToPublish.length;
          // 發(fā)布包的名稱以及版本,用于輸出展示
          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 發(fā)包的整個過程的。

          首先可以看到上面代碼中,我通過注釋將這個步驟分成了六步:

          1. 驗證 npm && 項目license

          首先上面可以看到,這一步分為兩個方法,一步是做 npm 相關(guān)的驗證: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));
          // 根據(jù) username 對要發(fā)布的包做個鑒權(quán)
          chain = chain.then(username => {
          if (username) {
          return verifyNpmPackageAccess(this.packagesToPublish, username, this.conf.snapshot);
          }
          });

          // 校驗用戶是否需進行 2fa 的驗證 -- 安全驗證相關(guān)
          chain = chain.then(() => getTwoFactorAuthRequired(this.conf.snapshot));
          chain = chain.then(isRequired => {
          // 記錄一下
          this.twoFactorAuthRequired = isRequired;
          });
          }

          return chain;
          }

          prepareRegistryActions 執(zhí)行時會先去校驗 registry,如果是第三方的 registry,會停止校驗,用戶在發(fā)包設(shè)置了 no-verify-access 就不進行后面校驗,默認會校驗。

          校驗過程是首先通過 getNpmUsername 去拿到用戶的 username,這里是通過 npm 提供的相關(guān)接口來獲取,具體流程可以自行參考。拿到 username 之后根據(jù) username 以及本次 publish 中需要發(fā)布的包的信息去做一個鑒權(quán),判斷用戶是否用該包的讀寫發(fā)包權(quán)限,沒有就會拋錯,最后一步是個 2fa 的驗證,一般 npm 包都不會開啟,主要是為了安全作用做二次驗證使用,這里不做具體講解。

          下面在看 license 的校驗過程,方法是 prepareLicenseActions :

          prepareLicenseActions() {
          return Promise.resolve()
          // 通過 glob 的方式去找到待發(fā)布的包中沒有 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;
          }
          });
          }

          這一步并不會對主要流程有什么影響,主要就是找目前待發(fā)布的包中沒有 license 的,然后給個 warnning 提示,這里找的方式使用過 lerna 自己構(gòu)造的 project graph 去篩待發(fā)布包中不存在 liecense 文件的路徑,想了解具體過程參考 getPackagesWithoutLicense。

          2. 更新本地依賴版本 && 待發(fā)布包 gitHead

          可以你會對更新本地依賴版本這一步可能會有些迷惑,這里舉個例子來解釋一下,在 lerna 中,如果 workspaces 之前存在依賴的話,在這次發(fā)包中,例如 A 這個包依賴了 B,B 在這次發(fā)包中版本升級了,那么這里 A 里面依賴的 B 也要更新到對應(yīng)的版本。

          來看一下這一步:

          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")
          );

          // 拿到上一步結(jié)果之后,就把對應(yīng)的更新寫入 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 就要更新到發(fā)包的版本
          node.pkg.updateLocalDependency(resolved, depVersion, this.savePrefix);
          }
          });
          }

          這里涉及到的一些操作方法,都是來自于 lerna 構(gòu)建的 project graph,這部分可以去參考一下 lerna core 中源碼。

          這里的 gitHead 是一個 hash 值,用戶可以通過 --git-head 來自行指定,如果不指定的話,lerna 這里會默認幫你取當前 commit 的 hash 值,即通過 git rev-parse HEAD 來獲取,一般 gitHead 結(jié)合 from-package 來使用,先看看代碼:

          annotateGitHead() {
          try {
          // 用戶如果沒有默認指定就使用最近的 commit hash 值
          // getCurrentSHA 就是執(zhí)行了一次 git rev-parse HEAD
          const gitHead = this.options.gitHead || getCurrentSHA(this.execOpts);
          for (const pkg of this.packagesToPublish) {
          // gitHead 是用來關(guān)聯(lián) package 和 git 記錄
          // npm publish 正常情況下需要該字段
          pkg.set("gitHead", gitHead);
          }
          } catch (err) {
          }
          }

          在使用 from-package 的方式進行發(fā)包的時候,會把這個 githead 字段寫在 package.json 里面。

          3. 更新寫入本地

          這一步就是將第二步的一些更新直接寫到 lerna 中對應(yīng)項目里面去,即寫到磁盤里面,主要的方法為:

          serializeChanges() {
          return pMap(this.packagesToPublish, pkg => pkg.serialize());
          }

          這個 pkg.serialize() 方法,是可以在 lerna 的 core 中找到的,主要作用就是將相關(guān)的更新寫入本地磁盤:

          serialize() {
          // 這里的 writePkg 封裝了 write-package-json 這個方法
          return writePkg(this.manifestLocation, this[PKG]).then(() => this);
          }

          4. package pack

          在講解之前,我們得先知道 npm pack 這個操作是干什么的,它會打包當前的文件夾內(nèi)容打包成一個 tar 包,我們在執(zhí)行 npm publish 的時候會經(jīng)常看到這個操作:

          不過 npm publish 幫我們封裝了這個過程,lerna publish 中也會有這個過程,這已經(jīng)是發(fā)包前的最后一個操作了,具體可參考代碼:

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

          // ...
          const opts = this.conf.snapshot;
          const mapper = pPipe(
          [
          // packDirectory 會給對應(yīng) pkg 的文件夾打個 tar 包出來
          // 類似于上面的 npm pack
          pkg =>
          pulseTillDone(packDirectory(pkg, pkg.location, opts)).then(packed => {
          pkg.packed = packed;
          return pkg.refresh();
          }),
          ].filter(Boolean)
          );

          // 這里會按照拓撲序列去對要發(fā)布的包進行 pack
          chain = chain.then(() => this.topoMapPackages(mapper));

          return pFinally(chain, () => tracker.finish());
          }

          這一步首先可以參考 topoMapPackages 這個方法,他會按照拓撲順序去對需要更新的包進行 pack,這里 publish 因為涉及到包之間的一些依賴關(guān)系,因此只能按照拓撲的順序去執(zhí)行,this.packagesToPublish 里面存的是待發(fā)布的包:

          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",
          });
          }

          因此這里會按照拓撲順序去對要發(fā)布的包進行打包成 tar 包的操作,具體執(zhí)行方法是 packDirectory 這個方法,這個方法我只貼一下打包的那一段邏輯,還有一些其他的預處理邏輯做了一下刪除:

          const tar = require("tar");
          const packlist = require("npm-packlist");

          function packDirectory(_pkg, dir, _opts) {
          // ...
          let chain = Promise.resolve();
          // 拿到待發(fā)布 pkg 的 目錄信息
          chain = chain.then(() => packlist({ path: pkg.contents }));
          // 對目錄下面的一些文件夾打包
          chain = chain.then(files =>
          // 具體參數(shù)去參考 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 形式寫到一個臨時目錄下面
          // 發(fā)布完了會刪除
          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;
          }

          5. Package publish

          在上一步完成了待發(fā)布包的打包操作之后,這一步就是 lerna publish 整個流程的最后一步了!

          這一步會將上一次打包的內(nèi)容直接發(fā)布出去,先來看一下代碼:

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

          // 前面說過的 2fa 2次驗證,這里會驗證一下
          if (this.twoFactorAuthRequired) {
          chain = chain.then(() => this.requestOneTimePassword());
          }

          const opts = Object.assign(this.conf.snapshot, {
          // 設(shè)置了 tempTag 就先用 lerna-temp
          tag: this.options.tempTag ? "lerna-temp" : this.conf.get("tag"),
          });

          const mapper = pPipe(
          [
          pkg => {
          // 拿到 pkg 上一次發(fā)布的 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 });

          // 發(fā)布包這個操作通過 npmPublish 這個過程來完成
          return pulseTillDone(npmPublish(pkg, pkg.packed.tarFilePath, pkgOpts, this.otpCache)).then(() => {
          return pkg;
          });
          }
          ].filter(Boolean)
          );

          // 這里和上一步 pack 一樣,按照拓撲執(zhí)行
          chain = chain.then(() => this.topoMapPackages(mapper));

          return pFinally(chain, () => tracker.finish());
          }

          上一步講了 topoMapPackages 這個方法,這里同樣的,它會按照拓撲順序去發(fā)布待發(fā)布的 pkg。

          在 npmPublish 這個方法中,會將前面打包的 pkg 的 tar 包 publish 到 npm 上面去,這里用的是 lerna 作者自己的一個包,感興趣的可以去 npm 上搜一下:@evocateur/libnpmpublish

          這個包可以不用擔心 tarball 打包自于哪個 pkg,只要你有個 tarball 它會幫你直接上傳到 npm 上面去來完成一次發(fā)布,具體的內(nèi)容可以在 npm 中找到。

          這里因為引入了一些外部包加上這里有太多的邊界條件處理,這里就不具體去看 npmPublish 這個方法了,貼上發(fā)布的那部分代碼,可以參考一下:

          const { publish } = require("@evocateur/libnpmpublish");

          return otplease(innerOpts => publish(manifest, tarData, innerOpts), opts, otpCache).catch(err => {
          // re-throw to break chain upstream
          throw err;
          });

          那么再走到這一步結(jié)束之后,基本上整個 lerna 的發(fā)包流程都走完了。

          后續(xù)的一些收尾工作的處理,可以再拉回 執(zhí)行(execute) 這一節(jié)開頭的代碼分析那里。

          總結(jié)

          本文從源碼角度剖析了一下 lerna publish 的執(zhí)行機制,對于一些邊界的 corner case 有些刪減,按照主線講解了 lerna publish 是怎么完成 lerna monorepo 中的整個發(fā)包流程操作的。希望本系列的文章能對你有所幫助。

          END



          如果覺得這篇文章還不錯
          點擊下面卡片關(guān)注我
          來個【分享、點贊、在看】三連支持一下吧

             “分享、點贊、在看” 支持一波  


          瀏覽 141
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  91嫩|婷婷丨入口图片 | 一区二区三区四区无码高清 | 激情视频噜噜 | 亚洲欧美色图在线 | 日韩久久久久 |