lerna 包管理工具

4,407 阅读14分钟

lerna 解决的问题

背景

维护过多个 package 的时候,会遇到一个选择:这些 package 是放在一个仓库里维护还是放在多个仓库里单独维护?

数量较少的时候,多个仓库维护不会有太大问题,但是当 package 数量逐渐增多时,一些问题逐渐暴露出来:

  1. packages 之间相互依赖,开发繁琐:开发人员需要在本地手动执行npm link。
  2. 需要单独发布每个 package:修改多个包中内容时,每个包都需要单独执行 npm publish。
  3. issue 难以统一追踪、管理:因为其分散在独立的 repo 里,issue 需要从不同的 git 仓库去查找、维护。
  4. 每次安装 node_modules 需要占用非常大的时间和空间::每一个 package 都包含独立的 node_modules,而且大部分都包含babel, webpack 等开发时依赖,安装耗时冗余并且占用过多空间。

lerna

lerna 是用于管理具有多个包的 JavaScript 项目的工具,对于上面提到的问题 lerna 都提供了对应的解决方式。

  1. 自动解决 packages 之间的依赖关系。
  2. 一句命令自动发布有改动的 package,自动维护各 package 版本号的更替。
  3. 将多包的 git 记录合并到一起,用一个 git 仓库管理。
  4. 将公用 package 安装到全局,只用安装一次,减少重复安装。

lerna 使用方法

全局安装

npm i -g lerna

初始化

初始化一个 lerna 项目有2种方式,固定模式和独立模式

  1. 固定模式(Fixed mode),packages下的所有包共用一个版本号(version),默认为固定模式.
mkdir lerna-learning
cd lerna-learning
lerna init

我们来看下初试化后的项目,共3项内容,一个package.json,一个lerna的配置文件,和一个空的packages包

  • 目录结构:

|-- packages/

|-- package.json

|-- lerna.json

  • package.json
{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}
  • lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}
  1. 独立模式(Independent mode),每一个包有一个独立的版本号
mkdir lerna-learning
cd lerna-learning
lerna init --independent

独立模式产生的唯一不同就是,lerna.json 的 version:0.0.0 改为了 version:"independent" 。所以,如果需要将更改固定模式为独立模式,只需要手动将 version 值修改为 "independent"。

添加包

lerna create module-1
lerna create module-2
lerna create module-3

添加后,在packages/下面会多3个包,包内容如下:

你也可以 import 一个已经存在的包,需要注意的是,由于lerna会把包中的 git 记录合并下来,所以你的 lerna 项目和包中都需要先提交 git commit ,否则 import 会报错

lerna import package1

公用包

添加

lerna add vue

顶层的lerna.json、package.json,node_modules中不会有任何变化,但packages下的每个modules的package.json中(packages/module-1/package.json、packages/module-2/package.json、packages/module-3/package.json)

都会增加一行:

    "vue": "^2.6.12"

且在每个包中安装三方包。到这里,会有一个疑问,不是说 lerna 会解决重复依赖问题吗?需要在 lerna.json 中配置一下:

{
  "command": {
    "bootstrap": {
      "hoist": true
    }
  },
  "packages": [
    "packages/*"
  ],
  "version": "0.0.1-alpha.7"
}

add 的时候是在每个包中增加三方包,加上这个配置才能去除重复安装,而为什么会是配置 bootstrap,我们在源码解析中会讲到。

下载

lerna boostrap

每个子包的 node_modules 下面都会安装公共包 vue

lerna boostrap --hoist

只有和lerna.json 同级的 node_modules 中会安装 vue,其他包中不安装

模块间依赖

用法

将 packages/module-1 安装到除 module-1 以外的所有模块

lerna add module-1

lerna add module-1 会在module-2、moudule-3等等,其他的pakages/下面的包中全部装上module-1。在module-2和module-3的 package.json 中,dependencies会增加一行:

"dependencies": {
    "module-1": "^0.0.0",
  }

将 module-1 单独安装到 module-2

lerna add module-1 --scope=module-2

将 module-1 单独安装到 module-2 的 devDependencies 下

lerna add module-1 --scope=module-2 --dev 

效果

module2 中引用了 module1 :

module.exports = module2;
var module1 = require('module-1')
function module2() {
    module1();
}

我们看下 lerna add module-1 之后 module-2的node_modules目录

module-2 中安装了module-1且通过软链接的方式连接,相当于这个效果:

cd module-1
npm link

cd module-2
npm install module-1
npm link module-1

lerna 发布及打包

直接运行

在根目录直接运行lerna publish,根据提示进行版本号选择(可以选择 alpha 版本、release 版本等)

lerna publish

发布成功

Successfully published:
 - module-1@0.0.1-alpha.0
 - module-2@0.0.1-alpha.0
 - module-3@0.0.1-alpha.0

因为是全新代码 module-1、module-2、module-3 全部发布,我们可以在 npm 查到我们 3 个新鲜发布的包。

与git配合

我这边已经发布了很多次包了,现在各个基础版本如下,lerna.json中的版本为:

  "version": "0.0.1-alpha.11"

module-1和moduel-2中的package.json 都为:

"version":"0.0.1-alpha.11"

接下来,我们修改module-1中的代码,执行:

lerna publish

module-1发布成功:

则发布时,lerna.json中的版本号自动更新为:

  "version": "0.0.1-alpha.12"

由于我们只修改了module-1代码,所以module-1 的 package.json 中的版本号也自动更新为:

  "version": "0.0.1-alpha.12"

其他packages下的包的则完全不变动。也就是说现在,module-2的package.json中的version 还是

"version":"0.0.1-alpha.11"

再次发布时,lerna.json中的版本为,则在aphpa.12的基础上增加版本号,这个是什么意思呢?我们来修改一下module-2的代码,然后执行

lerna publish

module-2发布成功

lerna.json 从 0.0.1-alpha.12 >> 0.0.1-alpha.13,而module-2直接从原来的版本升级为lerna的版本:0.0.1-alpha.13,

module-2的package.json

  "version": "0.0.1-alpha.13" // 0.0.1-alpha.11 >> 0.0.1-alpha.13

这一点就是lerna根据 git 记录判断对应package是否改变,从而动态发布的结果,只发布改动过的包~

小tips:

由于lerna是根据git动态发布,所以每次执行lerna publish时,都要先提交git哦!

随意版本

lerna publish 能否随意发布各种版本的包?用这个命令,就会根据各个项目下面的package.json发布包啦

lerna publish from-package

批量命令

lerna bootstrap

安装依赖,这个命令相当于 在每个packages/的包中,执行了npm install

lerna bootstrap

lerna run

lerna run 这个命令会跑所有packages/下面的包中的相同命令,相当于 在每个packages/的包中,执行了npm run dev

lerna run dev

常用 lerna 命令

  lerna add <pkg> [globs..]  Add a single dependency to matched packages
  lerna bootstrap            Link local packages together and install remaining package dependencies
  lerna changed              List local packages that have changed since the last tagged release                                                                                        [aliases: updated]
  lerna clean                Remove the node_modules directory from all packages
  lerna create <name> [loc]  Create a new lerna-managed package
  lerna diff [pkgName]       Diff all packages or a single package since the last release
  lerna exec [cmd] [args..]  Execute an arbitrary command in each package
  lerna import <dir>         Import a package into the monorepo with commit history
  lerna info                 Prints debugging information about the local environment
  lerna init                 Create a new Lerna repo or upgrade an existing repo to the current version of Lerna.
  lerna link                 Symlink together all packages that are dependencies of each other
  lerna list                 List local packages                                                                                                                                     [aliases: ls, la, ll]
  lerna publish [bump]       Publish packages in the current project.
  lerna run <script>         Run an npm script in each package that contains that script
  lerna version [bump]       Bump version of packages changed since the last release.

我们来分一下类,顺便翻译一下:

-- 项目相关命令

lerna init 初始化一个lerna的项目
lerna create 创建一个子package

lerna import 将已有项目导入为子package

lerna list 列出当前lerna项目有包含哪些packages


-- 依赖相关命令

lerna add 添加npm依赖

lerna clean 删除node_modules

lerna bootstrap 安装依赖,并通过symbol link的方式来链接子packages之间的依赖


-- git相关命令

lerna diff 跟git diff类似

lerna changed 列出当前有哪些packages发生过变更


-- 执行相关命令

lerna exec 在某个package下执行任意shell命令

lerna run 在某个package下执行npm scripts


-- npm相关命令

lerna link 将彼此依赖的子packages链接起来

lerna publish 发布package,同npm publish

lerna version 让用户确认发布的版本号

我们通过源码来看一下lerna的重要的命令都做了什么,怎么实现的:

git 链接: github.com/lerna/lerna…

源码解读

命令主入口

  "bin": {
    "lerna": "cli.js"
  },

lerna/cli.js

#!/usr/bin/env node
const importLocal = require("import-local");
// 如果可用,让全局安装的包使用其本地安装的版本
// https://www.npmjs.com/package/import-local 这里不再赘述
if (importLocal(__filename)) {
  // 本地lerna
  require("npmlog").info("cli", "using local version of lerna");
} else {
  //这个写法蛮特别的,其实就是require("./index")("add/bootstrap/...其他command")
  require(".")(process.argv.slice(2));
}

lerna 的 index.js 文件,主要是引入lerna/cli并注册了一堆命令

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

那么这个lerna cli是做什么的呢?它主要是引入了yargs和将lerna的cli创建了一个命令行交互帮助:

"use strict";

const dedent = require("dedent");
const log = require("npmlog");
const yargs = require("yargs/yargs");
const globalOptions = require("@lerna/global-options");

module.exports = lernaCLI;

/**
 * A factory that returns a yargs() instance configured with everything except commands.
 * Chain .parse() from this method to invoke.
 *
 * @param {Array = []} argv
 * @param {String = process.cwd()} cwd
 */
function lernaCLI(argv, cwd) {
  const cli = yargs(argv, cwd);

  return globalOptions(cli)
    .usage("Usage: $0 <command> [options]")
    .demandCommand(1, "A command is required. Pass --help to see all available commands and options.")
    .recommendCommands()
    .strict()
    .fail((msg, err) => {
      // certain yargs validations throw strings :P
      const actual = err || new Error(msg);

      // ValidationErrors are already logged, as are package errors
      if (actual.name !== "ValidationError" && !actual.pkg) {
        // the recommendCommands() message is too terse
        if (/Did you mean/.test(actual.message)) {
          log.error("lerna", `Unknown command "${cli.parsed.argv._[0]}"`);
        }

        log.error("lerna", actual.message);
      }

      // exit non-zero so the CLI can be usefully chained
      cli.exit(actual.code > 0 ? actual.code : 1, actual);
    })
    .alias("h", "help")
    .alias("v", "version")
    .wrap(cli.terminalWidth()).epilogue(dedent`
      When a command fails, all logs are written to lerna-debug.log in the current working directory.

      For more information, find our manual at https://github.com/lerna/lerna
    `);
}

熟悉 yargs 的同学一定知道,Yargs 就是一个帮助我们解析命令行参数、生成优雅用户交互界面的构建交互式命令行工具。如果不熟悉的同学也没关系,这边链接了yargs的官网可以自己浏览一下:

www.npmjs.com/package/yar…

github.com/yargs/yargs…

那我们直接来看看各个命令吧

1. add 命令 :安装npm包

带着2个问题去看这个命令:1.如何解决的包之间的依赖,2.公共包重复安装问题的?

add/command.js 构建命令行交互,处理 add 各命令

"use strict";

const filterable = require("@lerna/filter-options");

/**
 * @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
 */
exports.command = "add <pkg> [globs..]";

exports.describe = "Add a single dependency to matched packages";

exports.builder = yargs => {
  yargs
    .positional("pkg", {
      describe: "Package name to add as a dependency",
      type: "string",
    })
    .positional("globs", {
      describe: "Optional package directory globs to match",
      type: "array",
    })
    .options({
      D: {
        group: "Command Options:",
        type: "boolean",
        alias: "dev",
        describe: "Save to devDependencies",
      },
      E: {
        group: "Command Options:",
        type: "boolean",
        alias: "exact",
        describe: "Save version exactly",
      },
      P: {
        group: "Command Options:",
        type: "boolean",
        alias: "peer",
        describe: "Save to peerDependencies",
      },
      registry: {
        group: "Command Options:",
        describe: "Use the specified registry for all npm client operations.",
        type: "string",
        requiresArg: true,
      },
      "no-bootstrap": {
        group: "Command Options:",
        describe: "Do not automatically chain `lerna bootstrap` after changes are made.",
        type: "boolean",
      },
      bootstrap: {
        // proxy for --no-bootstrap
        hidden: true,
        type: "boolean",
      },
    })
    .example(
      "$0 add module-1 packages/prefix-*",
      "Adds the module-1 package to the packages in the 'prefix-' prefixed folders"
    )
    .example("$0 add module-1 --scope=module-2", "Install module-1 to module-2")
    .example("$0 add module-1 --scope=module-2 --dev", "Install module-1 to module-2 in devDependencies")
    .example("$0 add module-1 --scope=module-2 --peer", "Install module-1 to module-2 in peerDependencies")
    .example("$0 add module-1", "Install module-1 in all modules except module-1")
    .example("$0 add module-1 --no-bootstrap", "Skip automatic `lerna bootstrap`")
    .example("$0 add babel-core", "Install babel-core in all modules");

  return filterable(yargs);
};

exports.handler = function handler(argv) {
  return require(".")(argv);
};

add/index.js 实现 add 命令各处理函数:

// 继承了 Command 函数,我们先来浏览一下 AddCommand 有哪些函数
class AddCommand extends Command {
    // 是否需要为一个git仓库
    requiresGit() {
    	return false;
    }
    // dependencyType 依赖形式
    dependencyType() {
      ...
      return "dependencies";
    }
    
    // 初始化函数
    initialize() {}
    // 执行函数
    execute() {}
    
    // 收集需要改变的依赖
    collectPackagesToChange() {}
    // 更改package
    makeChanges() {}
    // 获取依赖树
    getPackageDeps(pkg) {}
    // 获取包版本
    getPackageVersion() {}
    // 检测包是否存在
    packageSatisfied() {}
}

其他的工具函数我们暂时不看,我们主要先来看下这个 父 class Command,以及 initialize和execute

class Command {
  constructor(_argv) {
    ...
    日志打印 && 检测名称代码
    ...
    
    // launch the command
    let runner = new Promise((resolve, reject) => {
      // run everything inside a Promise chain
      let chain = Promise.resolve();

      chain = chain.then(() => {
        this.project = new Project(argv.cwd);
      });
      // 执行lerna的各类配置
      chain = chain.then(() => this.configureEnvironment());
      chain = chain.then(() => this.configureOptions());
      chain = chain.then(() => this.configureProperties());
      chain = chain.then(() => this.configureLogging());
      chain = chain.then(() => this.runValidations());
      chain = chain.then(() => this.runPreparations());

	  // 执行command命令
      chain = chain.then(() => this.runCommand());

      chain.then(
        result => {
          warnIfHanging();

          resolve(result);
        },
        err => {
          ...
          catch 各类错误,并打日志
          ...
          
          reject(err);
        }
      );
    });

    // 执行 runner Promise 
    if (argv.onResolved || argv.onRejected) {
      runner = runner.then(argv.onResolved, argv.onRejected);

      // when nested, never resolve inner with outer callbacks
      delete argv.onResolved; // eslint-disable-line no-param-reassign
      delete argv.onRejected; // eslint-disable-line no-param-reassign
    }

    // "hide" irrelevant argv keys from options
    for (const key of ["cwd", "$0"]) {
      Object.defineProperty(argv, key, { enumerable: false });
    }

    Object.defineProperty(this, "argv", {
      value: Object.freeze(argv),
    });

    Object.defineProperty(this, "runner", {
      value: runner,
    });
  }
  
  runCommand() {
    return Promise.resolve()
       // 执行 command 初始化
      .then(() => this.initialize())
      .then(proceed => {
        if (proceed !== false) {
          // 执行 command 初始化
          return this.execute();
        }
        // early exits set their own exitCode (if non-zero)
      });
  }

  initialize() {
    throw new ValidationError(this.name, "initialize() needs to be implemented.");
  }

  execute() {
    throw new ValidationError(this.name, "execute() needs to be implemented.");
  }
}

add 命令的 initialize 和 execute

// 初始化一些变量
initialize () {
	...
    
	// 初始化变量
    this.selfSatisfied = this.packageSatisfied();
    
    ...

    if (this.packageGraph.has(this.spec.name) && !this.selfSatisfied) {
      const available = this.packageGraph.get(this.spec.name).version;
      ...
    }
    ...
    // 获取版本号
    chain = chain.then(() => this.getPackageVersion());
    
    // 过滤掉属于自己内部的包
    chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
    
    // 整理需要装的包及版本号
    chain = chain.then(() => this.collectPackagesToChange());
  }

execute

execute () {
  ...
  // 收集要安装的包的 dev
  chain = chain.then(() => this.makeChanges());
  ...
  // 调用lerna 的 bootstrap 函数
  return bootstrap(argv);
  ...
}
makeChanges() {
	const deps = this.getPackageDeps(pkg);
}

目前为止,我们是还没有找到这两个问题的答案的,1.如何解决的包之间的依赖,2.公共包重复安装问题的?,因为add 最后调用了bootstrap,所以我们接着带着这两个问题来看 bootstrap 源码

2. bootstrap:安装依赖

github.com/lerna/lerna…

initialize 中主要是初始化一些 dep 树的关系,我们直接来看 execute 的重要内容

execute() {
...
	tasks.push(
    	// 收集依赖
      () => this.getDependenciesToInstall(),
      // 安装依赖
      result => this.installExternalDependencies(result),
      // 建立软连
      () => this.symlinkPackages()
    );
 ...
 }

我们来看下 getDependenciesToInstall 的内容

// 初始化依赖容器,在之后解析出需要安装的依赖给其赋值
getDependenciesToInstall(){
const depsToInstall = new Map();
...
// 遍历依赖,
for (const [externalName, externalDependents] of depsToInstall) {
  let rootVersion
  // 若开启了hoist功能并且该依赖在packages中出现了多次
  if (this.hoisting && isHoistedPackage(externalName, this.hoisting)) {
    // 检测该依赖出现次数最多的版本
    const commonVersion = Array.from(externalDependents.keys()).reduce((a, b) =>
      externalDependents.get(a).size > externalDependents.get(b).size ? a : b
    );
    // 得到需要安装在根依赖中的版本,并取得它所依赖的其他包
    rootVersion = rootExternalVersions.get(externalName) || commonVersion;
    const dependents = Array.from(externalDependents.get(rootVersion)).map(
      leafName => this.targetGraph.get(leafName).pkg
    );
    externalDependents.delete(rootVersion);
    // 设置依赖的最佳版本
    rootActions.push(() =>
      hasDependencyInstalled(rootPkg, externalName, rootVersion).then(isSatisfied => {
        rootSet.add({
          name: externalName,
          dependents,
          dependency: `${externalName}@${rootVersion}`,
          isSatisfied,
        });
      })
    );
  }
  // 将其他出现次数较少的版本安装在原始的叶子节点下
  for (const [leafVersion, leafDependents] of externalDependents) {
    for (const leafName of leafDependents) {
      const leafNode = this.targetGraph.get(leafName);
      const leafRecord = leaves.get(leafNode) || leaves.set(leafNode, new Set()).get(leafNode);
      // 设置未安装的依赖
      leafActions.push(() =>
        hasDependencyInstalled(leafNode.pkg, externalName, leafVersion).then(isSatisfied => {
          leafRecord.add({
            dependency: `${externalName}@${leafVersion}`,
            isSatisfied,
          });
        })
      );
    }
  }
}

}

到现在为止,我们已经能够解释 lerna 如何解决公共包依赖的问题了,在 hoist 模式下只会在顶层依赖中安装公用包。 我们再来看下软链接的问题,前面已经说过,bootstrap 的 execute 中会执行 symlinkPackages

symlinkPackages() {
    return symlinkDependencies(
      this.filteredPackages,
      this.targetGraph,
      this.logger.newItem("bootstrap dependencies")
    );
  }
  
symlinkDependencies (){
  ...
  // 如果原来有对应路径,直接处理软链接
  if(dirExists){
      const isDepSymlink = resolveSymlink(targetDirectory);
  }
  ...
  // 没有对应路径,通过解析得到对应路径后,建立软链接
  chain = chain.then(() => createSymlink(dependencyLocation, targetDirectory, "junction"));
}

中间的处理过程不再赘述,最终createSymlink 调用 node 原生的 fs 建立软链接

function createSymbolicLink(src, dest, type) {
...
  return fs
    ...
    .then(() => fs.symlink(src, dest, type));
}
...

到目前,我们已经能够回答之前提出的另一个问题,lerna 如何解决模块之间的依赖

3. import 命令:引入文件 + 引入git commits

同样我们带着一个问题去看这个源码:import 如何引入git commits 的

github.com/lerna/lerna… 我们先来看下import的效果

a.在packages下增加了一个文件夹,放入项目。

b.保存所有git的记录。

与其他命令不同,import 命令中,initialize 做了大部分工作,而 excute 只是执行了简单的命令,catch 一些异常

// 解析文件夹
initialize() {
    const inputPath = this.options.dir;
    const externalRepoPath = path.resolve(inputPath);
    const externalRepoBase = path.basename(externalRepoPath);
    stats = fs.statSync(externalRepoPath);
    ...
    异常处理,例如:No repository found at 等
    ...
    
// 1.添加文件夹
	this.targetDirRelativeToGitRoot = path.join(lernaRootRelativeToGitRoot, targetDir);
    
// 2.保留 git 节点的父节点,取出 git log
	this.commits = this.externalExecSync("git", this.gitParamsForTargetCommits())
      .split("\n")
      .reverse();
}

gitParamsForTargetCommits() {
    const params = ["log", "--format=%h"];
    if (this.options.flatten) {
      params.push("--first-parent");
    }
    return params;
  }
 execute() {
 try {
	//将所有 log 在保留时间及作者的情况下,重新提交一遍
	procArgs.push("--committer-date-is-author-date");
    const proc = ChildProcessUtilities.exec("git", procArgs, this.execOpts);
  }
  catch(e){
  ...
  }
 }

如下图,git的md5戳已经改变,并不是原来的git commit,只是又提交了一遍。

4. publish 命令:发布packages

github.com/lerna/lerna…

lerna 如何做到只发布有更改的包?

init 调用栈

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

// ... 中间的层层封装不再赘述,最后是调用 diffSinceIn

function diffSinceIn(committish, location, opts) {
  const args = ["diff", "--name-only", committish];
  const formattedLocation = slash(path.relative(opts.cwd, location));

  if (formattedLocation) {
    args.push("--", formattedLocation);
  }
console.log('args:', args);
  log.silly("checking diff", formattedLocation);
  return childProcess.execSync("git", args, opts);
}

我们看下打出来的值:

args: [ 'diff', '--name-only', 'v0.0.1-alpha.7', '--', 'packages/module-1' ]
args: [ 'diff', '--name-only', 'v0.0.1-alpha.7', '--', 'packages/module-2' ]
args: [ 'diff', '--name-only', 'v0.0.1-alpha.7', '--', 'packages/module-3' ]

git diff --name-only 是什么意思呢?输入 git diff -h,show only names of changed files 也就是输出自 x 版本开始,更改过的文件的文件名

--name-only                          -- show only names of changed files 

当我修改了,module-1中/lib/module-1.js的值时,我们可以在控制台输入看一下输出 git diff --name-only v0.0.1-alpha.7 -- packages/module-1,

execute 只是执行,我们可以简单看一下


execute() {
	//...
    chain = chain.then(() => this.publishPacked());
}
publishPacked(){
  npmPublish(pkg, pkg.packed.tarFilePath, pkgOpts, this.otpCache)).then(() => {
    tracker.success("published", pkg.name, pkg.version);
  }
}

而 npmPublish 则是调用了 @evocateur/libnpmpublish 执行了npm publish

试图回答最初的问题,lerna是怎么解决下面的问题:

问:lerna 如何解决 packages 之间相互依赖,开发繁琐的问题?

答:在lerna add/bootstrap 时 lerna 内置了 npm link 功能,解决了模块间的依赖问题。

问:lerna 如何解决修改多个包的内容时,只需要执行一次 npm publish的?

答:lerna publish 通过 git diff 收集有改动的包,只发布有更改的包并自动管理各子包中的版本号。

问:lerna 如何解决 issue难以统一追踪、管理:因为其分散在独立的repo里,issue 需要从不同的 git 仓库去查找、维护的问题?

答:因为lerna把项目全部管理到在同一个仓库中,所以天然解决(课后作业:之前的issue是否会被保存?)。

问:lerna 如何解决每次安装node_modules需要占用非常大的时间和空间:每一个package都包含独立的node_modules,而且大部分都包含babel,webpack等开发时依赖,安装耗时冗余并且占用过多空间的问题?

答:lerna hoist 模式通过分析包依赖,只在顶层安装公共包,减少冗余安装。

参考链接

sosout.github.io/2018/07/21/…

github.com/lerna/lerna…

juejin.cn/post/684490…

segmentfault.com/a/119000001…

juejin.cn/post/684490…

juejin.cn/post/684490…

yrq110.me/post/devops…