Lerna源码解析

1,545 阅读2分钟

源码阅读准备:

  • 下载源码
  • 安装依赖
  • IDE 打开(webstorm)

源码阅读准备完成的标准

  • 找到入口文件
    • package.json bin 配置
"bin": {
    "lerna": "core/lerna/cli.js"
  }
  • 能够本地调试
    • 使用vscode调试方法
      1. 根目录新建.vscode目录
      2. 新建.vscode/launch.json文件
      {
        "configurations": [
          {
            "type": "node",
            "request": "launch",
            "name": "nodemon",
            "runtimeExecutable": "nodemon",
            "program": "${workspaceFolder}/core/lerna/cli.js",
            "restart": true,
            "console": "integratedTerminal",
            "internalConsoleOptions": "neverOpen",
            //参数是名称和值一组,多个参数,数组里添加即可,调试时会自动附加上去
            "args": ["list"]
          }
        ]
      }
      
    • 使用webstorm调试

解析入口文件

#!/usr/bin/env node

"use strict";

/* eslint-disable impo
rt/no-dynamic-require, global-require */
const importLocal = require("import-local");

if (importLocal(__filename)) {
  require("npmlog").info("cli", "using local version of lerna");
} else {
  require(".")(process.argv.slice(2)); //执行该步骤
}
  • require('.') 指的相对路径,当前目录下的index.js,相当于require('./index.js')
  • 查看index.js
"use strict";

const cli = require("@lerna/cli"); //指向了core/cli/index.js 几乎就是使用了yargs脚手架的命令,按理说应该是node_modules下面的@lerna/cli,但是core依赖对应package.json文件的依赖名称就就是lerna,所以直接找了core/cli/index.js 

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()
  //全部都是在注册yargs命令
    .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 ); //注入参数和版本号
}

  • 查看const cli = require("@lerna/cli");这个引用做了什么
    • 返回了globalOptions(cli)方法,应该就是注册了globalOptions的yargs
    • 查看globalOptions(cli)
"use strict";

const dedent = require("dedent"); //处理缩进的库
const log = require("npmlog"); 
const yargs = require("yargs/yargs");  //使用了yargs脚手架
const globalOptions = require("@lerna/global-options"); //进行globalOptions注册

module.exports = lernaCLI; 

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

  • 查看globaOptions
    1. 接收yargs参数,注册完成globalOptions之后又将yargs返回
"use strict";

const os = require("os");

module.exports = globalOptions;

function globalOptions(yargs) {
  // the global options applicable to _every_ command
  const opts = {
    loglevel: {
      defaultDescription: "info",
      describe: "What level of logs to report.",
      type: "string",
    },
    concurrency: {
      defaultDescription: os.cpus().length,
      describe: "How many processes to use when lerna parallelizes tasks.",
      type: "number",
      requiresArg: true,
    },
    "reject-cycles": {
      describe: "Fail if a cycle is detected among dependencies.",
      type: "boolean",
    },
    "no-progress": {
      describe: "Disable progress bars. (Always off in CI)",
      type: "boolean",
    },
    progress: {
      // proxy for --no-progress
      hidden: true,
      type: "boolean",
    },
    "no-sort": {
      describe: "Do not sort packages topologically (dependencies before dependents).",
      type: "boolean",
    },
    sort: {
      // proxy for --no-sort
      hidden: true,
      type: "boolean",
    },
    "max-buffer": {
      describe: "Set max-buffer (in bytes) for subcommand execution",
      type: "number",
      requiresArg: true,
    },
  };

  // group options under "Global Options:" header
  const globalKeys = Object.keys(opts).concat(["help", "version"]);

  return yargs
    .options(opts)
    .group(globalKeys, "Global Options:")
    .option("ci", {
      hidden: true, //隐藏option
      type: "boolean",
    });
}

入口文件初始化解析总结

  1. 使用yargs进行命令和参数的注册

  2. 注册globalOptions

    
    module.exports = globalOptions;
    
    function globalOptions(yargs) {
      // the global options applicable to _every_ command
      const opts = {
        loglevel: {
          defaultDescription: "info",
          describe: "What level of logs to report.",
          type: "string",
        },
        concurrency: {
          defaultDescription: os.cpus().length,
          describe: "How many processes to use when lerna parallelizes tasks.",
          type: "number",
          requiresArg: true,
        },
        "reject-cycles": {
          describe: "Fail if a cycle is detected among dependencies.",
          type: "boolean",
        },
        "no-progress": {
          describe: "Disable progress bars. (Always off in CI)",
          type: "boolean",
        },
        progress: {
          // proxy for --no-progress
          hidden: true,
          type: "boolean",
        },
        "no-sort": {
          describe: "Do not sort packages topologically (dependencies before dependents).",
          type: "boolean",
        },
        sort: {
          // proxy for --no-sort
          hidden: true,
          type: "boolean",
        },
        "max-buffer": {
          describe: "Set max-buffer (in bytes) for subcommand execution",
          type: "number",
          requiresArg: true,
        },
      };
    
      // group options under "Global Options:" header
      const globalKeys = Object.keys(opts).concat(["help", "version"]);
    
      return yargs
        .options(opts)
        .group(globalKeys, "Global Options:")
        .option("ci", {
          hidden: true,
          type: "boolean",
        });
    }
    
    
  3. 定义

  • 定义使用方法:.usage("Usage: $0 <command> [options]")

image.png

  • 定义至少需要接收一个参数:.demandCommand(1, "A command is required. Pass --help to see all available commands and options.")

image.png

  • 定义如果命令或者参数输入错误提示最相近的命令recommendCommands() image.png

  • 定义如果输入的命令没有,则提示未识别的参数:.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);
        })
    
    rainbow@MacBook-Pro-673 lerna % lerna D
    ERR! lerna 是指 ls?
    
  • 定义参数别名

    可以使用lerna -h lerna --help

    .alias("h", "help")
    .alias("v", "version")
    

image.png

  • 定义了命令内容的宽度为终端的整个宽度

    .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
        `);
    

image.png

  • 定义了各种命令

      .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 ls命令干了什么

查看lerna list实际上执行的是const listCmd = require("@lerna/list/command"); 查看这个引用

"use strict";

/**
 * @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
 */
exports.command = "link";

exports.describe = "Symlink together all packages that are dependencies of each other";

//builer:在执行命令之前注册参数
exports.builder = yargs => {
  yargs.options({
    "force-local": {
      group: "Command Options:",
      describe: "Force local sibling links regardless of version range match",
      type: "boolean",
    },
    contents: {
      group: "Command Options:",
      describe: "Subdirectory to use as the source of the symlink. Must apply to ALL packages.",
      type: "string",
      defaultDescription: ".",
    },
  });

//真正执行命令
  return yargs.command(
    "convert",
    "Replace local sibling version ranges with relative file: specifiers",
    () => {},
    handler
  );
};

//执行真正的命令
exports.handler = handler;
function handler(argv) {
  return require(".")(argv);
}

查看这块代码,也就是当前目录的index.js

function handler(argv) {
  return require(".")(argv);
}
"use strict";

const Command = require("@lerna/command");
const listable = require("@lerna/listable");
const output = require("@lerna/output");
const { getFilteredPackages } = require("@lerna/filter-options");

module.exports = factory;

function factory(argv) {
  return new ListCommand(argv);
}

class ListCommand extends Command {
  get requiresGit() {
    return false;
  }

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

    chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
    chain = chain.then(filteredPackages => {
      this.result = listable.format(filteredPackages, this.options);
    });

    return chain;
  }

  execute() {
    // piping to `wc -l` should not yield 1 when no packages matched
    if (this.result.text.length) {
      output(this.result.text);
    }

    this.logger.success(
      "found",
      "%d %s",
      this.result.count,
      this.result.count === 1 ? "package" : "packages"
    );
  }
}

module.exports.ListCommand = ListCommand;

查看继承的父类Command,父类中定义了constructor,执行constructor方法 core/command/index.js

class Command {
constructor(_argv) {
    log.pause();
    log.heading = "lerna";

    const argv = cloneDeep(_argv); //深拷贝参数
    log.silly("argv", argv);

    // "FooCommand" => "foo"
    this.name = this.constructor.name.replace(/Command$/, "").toLowerCase(); //类名称ListCommand

    // composed commands are called from other commands, like publish -> version
    this.composed = typeof argv.composed === "string" && argv.composed !== this.name;

    if (!this.composed) {
      // composed commands have already logged the lerna version
      log.notice("cli", `v${argv.lernaVersion}`);
    }

    // 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);
      });
      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()); //配置预检查内容
      chain = chain.then(() => this.runCommand()); //核心执行代码

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

          resolve(result);
        },
        err => {
          if (err.pkg) {
            // Cleanly log specific package error details
            logPackageError(err, this.options.stream);
          } else if (err.name !== "ValidationError") {
            // npmlog does some funny stuff to the stack by default,
            // so pass it directly to avoid duplication.
            log.error("", cleanStack(err, this.constructor.name));
          }

          // ValidationError does not trigger a log dump, nor do external package errors
          if (err.name !== "ValidationError" && !err.pkg) {
            writeLogFile(this.project.rootPath);
          }

          warnIfHanging();

          // error code is handled by cli.fail()
          reject(err);
        }
      );
    });

    // passed via yargs context in tests, never actual CLI
    /* istanbul ignore else */
    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"]) { // argv中注入"cwd", "$0"两个属性
      Object.defineProperty(argv, key, { enumerable: false });
    }

    Object.defineProperty(this, "argv", { //常规操作,注册this.argv
      value: Object.freeze(argv),
    });

    Object.defineProperty(this, "runner", { //注册this.runner
      value: runner,
    });
  }
}  

阅读Command核心代码

  • 如果子类中没有定义 initializeexecute方法就直接报错了
  runCommand() {
    return Promise.resolve()
      .then(() => this.initialize())
      .then(proceed => {
        if (proceed !== false) {
          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.");
  }
initialize() { //定义输出的内容
    let chain = Promise.resolve();

    chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
    chain = chain.then(filteredPackages => {
      this.result = listable.format(filteredPackages, this.options); //输出的内容格式化为字符串
    });

    return chain;
  }

  execute() {
    // piping to `wc -l` should not yield 1 when no packages matched
    if (this.result.text.length) {
      output(this.result.text); //终端输出的内容
    }

    this.logger.success( 
      "found", 
      "%d %s",
      this.result.count,
      this.result.count === 1 ? "package" : "packages"
    );
  }

image.png

Lerna项目本地调试另外一种方法

正常我们是使用npm link进行本地调试,如果包很多的话就比较混乱了,而且每次发布完成后都需要npm unlink ;

发布的时候Lerna使用publish命令参数将本地链接,解析为线上链接。

![image-20211101142340701](/Users/rainbow/Library/Application Support/typora-user-images/image-20211101142340701.png)

使用file的方法进行本地调试

在之前lerna-repo项目中;

packages/core

packages/utils

在core中引用‘@rainbow-cli-dev/utils’依赖

  "dependencies": {
    "@rainbow-cli-dev/utils": "file:../utils",
  },

在core目录npm i就会看到core目录的node_modules下创建了"@rainbow-cli-dev/utils"软链接。也可以正常使用依赖。

yargs脚手架用法

npm i yargs

#!/usr/bin/env node

const yargs = require("yargs/yargs");
const dedent = require("dedent");
const { hideBin } = require("yargs/helpers");
const cli = yargs(hideBin(process.argv))
  // dedent去除缩进
  cli
    .strict()
    .usage("Usage: $0 <command> [options]")
    .demandCommand(
      1,
      "A command is required. Pass --help to see all available commands and options."
    )
    .recommendCommands()
    .alias("h", "help")
    .alias("v", "version")
    // 定义多个option参数
    .options({
      debugger: {
        defaultDescription: "",
        describe: "bootstrap debugger mode",
        type: "boolean",
        alias: "d",
      },
    })
    // 注册单个option参数
    .option("register", {
      describe: "Define Global Option",
      type: "string",
      alias: "r",
    })
    .option("ci", {
      type: "boolean",
      hidden: true,
    })
    // 定义命令分组
    .group(["debugger"], "Dev Options")
    .group(["register"], "Extra Options")
    .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
    `).argv;

输入 rainbow-test -h

拿到输出

rainbow@MacBook-Pro-673 rainbow-test % rainbow-test -h
Usage: rainbow-test <command> [options]

Dev Command
  -d, --debugger  bootstrap debugger mode                                                           [布尔]

选项:
  -r, --register  Define Global Option                                                            [字符串]
  -h, --help      显示帮助信息                                                                      [布尔]
  -v, --version   显示版本号                                                                        [布尔]

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
rainbow@MacBook-Pro-673 rainbow-test % rainbow-test -h
Usage: rainbow-test <command> [options]

Dev Options
  -d, --debugger  bootstrap debugger mode                                                           [布尔]

Extra Options
  -r, --register  Define Global Option                                                            [字符串]

选项:
  -h, --help     显示帮助信息                                                                       [布尔]
  -v, --version  显示版本号                                                                         [布尔]

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