<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 publish

          共 22870字,需瀏覽 46分鐘

           ·

          2021-09-04 00:48

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

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

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

          同樣 lerna publish 也分為幾種不同的場(chǎng)景去運(yùn)行:

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

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

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

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

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

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

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

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

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

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

          設(shè)置屬性(configureProperties)

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

          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 會(huì)指定一個(gè)具體的 version 版本,而不會(huì)加上 npm 那邊的版本兼容前綴
          this.savePrefix = exact ? "" : "^";

          // 用于用戶自定義包的版本 tag 前綴,而不是使用默認(rèn)的 v
          this.tagPrefix = tagVersionPrefix;

          // --no-git-reset 用于避免 lerna publish 將暫存區(qū)的未提交的代碼都 push 到 git 上
          this.gitReset = gitReset !== false;

          // lerna 發(fā)包會(huì)默認(rèn)檢查用戶 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ù)的初始化,這里就不詳細(xì)介紹。

          初始化(initialize)

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

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

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

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

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

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

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

          // session 和 userAgent 都是 npm 發(fā)包需要驗(yàn)證的參數(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ā)包時(shí)候自定義 tag
          // 一般默認(rèn) tag 是 latest
          // lerna 中如果沒指定 --dist-tag, 正式包的 tag 會(huì)用 latest, --canary 的測(cè)試包會(huì)用 canary
          const distTag = this.getDistTag();

          // 如果該參數(shù)存在 會(huì)被注入進(jìn) npm conf 中
          if (distTag) {
          this.conf.set("tag", distTag.trim(), "cli");
          }

          // 注冊(cè)運(yùn)行 lerna.json 里面的 script 的 runner
          this.runPackageLifecycle = createRunner(this.options);

          // 如果 lerna 子 package 里面的 pkg.json 里面有 pre|post publish 這樣的 script
          // 會(huì)跳過 lifecycle script 的執(zhí)行過程,否則會(huì)去遞歸執(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)建一個(gè)執(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. 對(duì)方法返回的結(jié)果做一個(gè)處理

          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 的時(shí)候把 pkg.json 里面設(shè)置private 為 false 的包忽略掉
          this.updates = result.updates.filter(node => !node.pkg.private);
          // 需要更新的包以及對(duì)應(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 這個(gè) options
          if (this.options.contents) {
          // 把這些目錄寫進(jìn)需要發(fā)包的 pkg.json 中
          for (const pkg of this.packagesToPublish) {
          pkg.contents = this.options.contents;
          }
          }

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

          return true;
          });
          }

          initialize 前面有介紹主要分為三個(gè)步驟來執(zhí)行,因此 1、3 兩個(gè)步驟根據(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 進(jìn)行發(fā)包

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

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

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

          下面從這幾種情況做個(gè)介紹:

          from-git

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

          detectFromGit() {

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

          let chain = Promise.resolve();

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

          // 2. 拿到當(dāng)前 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 [];
          }
          // 獨(dú)立發(fā)包模式就拿到所有包的數(shù)組
          if (this.project.isIndependent()) {
          return taggedPackageNames.map(name => this.packageGraph.get(name));
          }

          // 固定模式只用拿到一個(gè)版本,所有的包都用一個(gè)版本
          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í)行過程還是比較簡(jiǎn)單明了的,在上面注釋中根據(jù)不同的方法執(zhí)行過程分為了 5 個(gè)步驟。主要就是根據(jù)當(dāng)前 commit 拿到 tags 里面的 packages 然后返回這些 packages 以及其版本信息。

          from-package

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

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

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

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

          // 3. 驗(yàn)證結(jié)果符合預(yù)期否
          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)于版本的比對(duì),從而得到需要更新的 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. 驗(yàn)證當(dāng)前 git 區(qū)是否干凈
          chain = chain.then(() => this.verifyWorkingTreeClean());

          // 2. 找到自上次來修改過的 packages 同時(shí)篩掉 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 會(huì)通過上一次的 version 來計(jì)算出這次的 version
          const nextVersion = semver.inc(lastVersion.replace(this.tagPrefix, ""), release.replace("pre", ""));
          return `${nextVersion}-${preid}.${Math.max(0, refCount - 1)}+${sha}`;
          };

          // 3. 根據(jù)不同的 mode 計(jì)算出包及版本相關(guān)參數(shù)
          if (this.project.isIndependent()) {
          // 獨(dú)立發(fā)包模式
          chain = chain.then(updates =>
          // pMap 是個(gè)鏈?zhǔn)綀?zhí)行過程,上一步的結(jié)果會(huì)給到下一步
          pMap(updates, node =>
          // 根據(jù) tag 匹配出需要發(fā)布的包
          describeRef(
          {
          match: `${node.name}@*`,
          cwd,
          },
          includeMergedTags
          )
          // 通過上面的 makeVersion 方法來計(jì)算發(fā)布的 canary 版本
          .then(makeVersion(node.version))
          // 返回出去,這里實(shí)際上就是個(gè) updateVerions 數(shù)組
          .then(version => [node.name, version])
          ).then(updatesVersions => ({
          updates,
          updatesVersions,
          }))
          );
          } else {
          // 固定的模式,那么所有的包都會(huì)使用一個(gè)版本(lerna.json 里面的版本)
          chain = chain.then(updates =>
          describeRef(
          {
          match: `${this.tagPrefix}*.*.*`,
          cwd,
          },
          includeMergedTags
          )
          // 只用一個(gè) version 去進(jìn)行計(jì)算
          .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 的處理過程或許看上去要復(fù)雜一些,其實(shí)不然,根據(jù)上面代碼注釋中的內(nèi)容可以比較清晰的看到整個(gè)執(zhí)行流程,不過多了幾種特殊情況需要去做一些判斷,其中比較復(fù)雜的第三步,是需要通過 tag 得到一些相關(guān)的信息,需要更新的包,然后針對(duì)這些包現(xiàn)有的版本去做一些計(jì)算,可以參考上面的 makeVersion 方法,這里就根據(jù) lerna 的 mode 分為了兩種情況。

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

          bump version

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

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

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

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

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

          執(zhí)行(execute)

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

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

          // 1. 驗(yàn)證 npm 源、權(quán)限,項(xiàng)目的 License 之內(nèi)
          chain = chain.then(() => this.prepareRegistryActions());
          chain = chain.then(() => this.prepareLicenseActions());

          if (this.options.canary) {
          // 如果是測(cè)試包,更新到測(cè)試包的 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. 對(duì) package pack
          chain = chain.then(() => this.packUpdated());

          // 5. 發(fā)布包
          chain = chain.then(() => this.publishPacked());

          if (this.gitReset) {
          // 設(shè)置了 --no-git-reset 會(huì)把 working tree 的版本修改重置
          // lerna 每次發(fā)包都會(huì)把更新的 package.json 的 version 的修改提交到 git 上去
          // 如果發(fā)測(cè)試包,這可能是沒必要,因此可以用這個(gè)選項(xiàng)把修改 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 的主要部分了,這一步的相對(duì)而言信息量比較巨大,我接下來會(huì)將上面的步驟拆一拆,一步一步來講解 execute 這一步是怎么完成 lerna 發(fā)包的整個(gè)過程的。

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

          1. 驗(yàn)證 npm && 項(xiàng)目license

          首先上面可以看到,這一步分為兩個(gè)方法,一步是做 npm 相關(guān)的驗(yàn)證:prepareRegistryActions

          prepareRegistryActions() {
          let chain = Promise.resolve();
          if (this.conf.get("registry") !== "https://registry.npmjs.org/") {
          // 這里的 registry 如果 url 是三方的例如公司的源,這里會(huì)跳過后面的檢查
          return chain;
          }

          // --no-verify-access 停止校驗(yàn),默認(rèn)會(huì)校驗(yàn)
          if (this.verifyAccess) {
          // 拿用戶的 npm username,拿不到在 getNpmUsername 會(huì)拋錯(cuò)
          chain = chain.then(() => getNpmUsername(this.conf.snapshot));
          // 根據(jù) username 對(duì)要發(fā)布的包做個(gè)鑒權(quán)
          chain = chain.then(username => {
          if (username) {
          return verifyNpmPackageAccess(this.packagesToPublish, username, this.conf.snapshot);
          }
          });

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

          return chain;
          }

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

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

          下面在看 license 的校驗(yàn)過程,方法是 prepareLicenseActions :

          prepareLicenseActions() {
          return Promise.resolve()
          // 通過 glob 的方式去找到待發(fā)布的包中沒有 licenses 的
          .then(() => getPackagesWithoutLicense(this.project, this.packagesToPublish))
          .then(packagesWithoutLicense => {
          // 對(duì)于沒有 liecense 的包會(huì)打個(gè) 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;
          }
          });
          }

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

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

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

          來看一下這一步:

          resolveLocalDependencyLinks() {
          // 先找到依賴過本地包的包
          // lerna 中 A, B 都是 workspace, A 依賴 B 引入的時(shí)候是通過 symlink 引入的
          // 因此這里找 B 的依賴包只用判斷 A 這里 resolved 的是不是個(gè)目錄就行
          const updatesWithLocalLinks = this.updates.filter(node =>
          Array.from(node.localDependencies.values()).some(resolved => resolved.type === "directory")
          );

          // 拿到上一步結(jié)果之后,就把對(duì)應(yīng)的更新寫入 A
          return pMap(updatesWithLocalLinks, node => {
          for (const [depName, resolved] of node.localDependencies) {
          // 注意這里 lerna 是不會(huì)處理 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 是一個(gè) hash 值,用戶可以通過 --git-head 來自行指定,如果不指定的話,lerna 這里會(huì)默認(rèn)幫你取當(dāng)前 commit 的 hash 值,即通過 git rev-parse HEAD 來獲取,一般 gitHead 結(jié)合 from-package 來使用,先看看代碼:

          annotateGitHead() {
          try {
          // 用戶如果沒有默認(rèn)指定就使用最近的 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 的方式進(jìn)行發(fā)包的時(shí)候,會(huì)把這個(gè) githead 字段寫在 package.json 里面。

          3. 更新寫入本地

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

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

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

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

          4. package pack

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

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

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

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

          // 這里會(huì)按照拓?fù)湫蛄腥?duì)要發(fā)布的包進(jìn)行 pack
          chain = chain.then(() => this.topoMapPackages(mapper));

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

          這一步首先可以參考 topoMapPackages 這個(gè)方法,他會(huì)按照拓?fù)漤樞蛉?duì)需要更新的包進(jìn)行 pack,這里 publish 因?yàn)樯婕暗桨g的一些依賴關(guān)系,因此只能按照拓?fù)涞捻樞蛉?zhí)行,this.packagesToPublish 里面存的是待發(fā)布的包:

          topoMapPackages(mapper) {
          // 這里是作者的一個(gè)注釋:
          // 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",
          });
          }

          因此這里會(huì)按照拓?fù)漤樞蛉?duì)要發(fā)布的包進(jìn)行打包成 tar 包的操作,具體執(zhí)行方法是 packDirectory 這個(gè)方法,這個(gè)方法我只貼一下打包的那一段邏輯,還有一些其他的預(yù)處理邏輯做了一下刪除:

          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 }));
          // 對(duì)目錄下面的一些文件夾打包
          chain = chain.then(files =>
          // 具體參數(shù)去參考 tar 這個(gè) 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 形式寫到一個(gè)臨時(shí)目錄下面
          // 發(fā)布完了會(huì)刪除
          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 整個(gè)流程的最后一步了!

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

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

          // 前面說過的 2fa 2次驗(yàn)證,這里會(huì)驗(yàn)證一下
          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 進(jìn)行判斷
          const preDistTag = this.getPreDistTag(pkg);
          // 取一下 tag,一般這里會(huì)取 opts.tag,針對(duì)于每個(gè)包的情況不同
          const tag = !this.options.tempTag && preDistTag ? preDistTag : opts.tag;
          // 這里 rewrite 一下 tag
          const pkgOpts = Object.assign({}, opts, { tag });

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

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

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

          上一步講了 topoMapPackages 這個(gè)方法,這里同樣的,它會(huì)按照拓?fù)漤樞蛉グl(fā)布待發(fā)布的 pkg。

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

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

          這里因?yàn)橐肓艘恍┩獠堪由线@里有太多的邊界條件處理,這里就不具體去看 npmPublish 這個(gè)方法了,貼上發(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é)束之后,基本上整個(gè) lerna 的發(fā)包流程都走完了。

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

          總結(jié)

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


          瀏覽 56
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  亚洲无免费码在线 | 欧美成a| 国产 无码 高潮 在线 | 亚洲无码精品视频 | 中文字幕成人视频 |