likes
comments
collection
share

脚手架(二):lerna

作者站长头像
站长
· 阅读数 69

Lerna简介和使用

Lerna是一个优化基于git+npm的多package项目的管理工具,现在也支持使用pnpm就行包管理

优势

  • 大幅减少重复操作
  • 提升操作的标准化

Lerna是架构优化的产物,它揭示了一个架构真理:项目复杂度提升后,就需要对项目进行架构优化。架构优化的主要目标往往都是以效能为核心。

lerna开发脚手架流程

脚手架(二):lerna

一般开发中最常使用的命令就是这些,其他命令可以去官网自行查询。

lerna+pnpm

pnpm对依赖包的管理相对于传统的npm和yarn有很大的提升,具体的优势大家可以参考:pnpm.io/zh/motivati…这里不再赘述。而lerna如今也已经支持使用pnpm来管理包,可以参考lerna.js.org/docs/recipe…来在lerna项目中使用pnpm

Lerna源码解析

源码调试(v5.6.2)

拉取源码安装依赖后,找到项目入口文件:core/lerna/cli.js,点击运行->添加配置->选择Node.js调试器

脚手架(二):lerna

此后会生成一个.vscode/launch.json文件,如果要进行命令调试,可以添加args参数,如下相当于执行lerna ls

脚手架(二):lerna

然后到调试模块运行对应的配置即可:

脚手架(二):lerna

断点调试与浏览器基本一致,这里不再赘述

命令执行流程(ls命令举例)

先看入口文件:core/lerna/cli.js

#!/usr/bin/env node

"use strict";

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

if (importLocal(__filename)) {
  require("npmlog").info("cli", "using local version of lerna");
} else {
  // require(".")相当于require("./index.js")
  // require("../")相当于require("../index.js")
  // 执行index.js文件中的main函数
  require(".")(process.argv.slice(2));
}

命令会来到else逻辑,执行index.js中的main函数,并将参数传入(这里的参数就是ls)

// core/lerna/index.js
// 命令入口函数
function main(argv) {
  const context = {
    lernaVersion: pkg.version,
  };

  // @ts-ignore
  return cli() // 执行cli函数
    .command(addCmd)
    .command(addCachingCmd)
    .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(repairCmd)
    .command(runCmd)
    .command(versionCmd)
    .parse(argv, context);
}

main函数也非常简单,返回cli函数执行的返回值(yargs对象),并注册了许多命令,继续看cli函数干了什么:

// core/cli/index.js
function lernaCLI(argv, cwd) {
  // 创建一个yargs实例
  const cli = yargs(argv, cwd);
  // 设置yargs的全局options
  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.exitCode > 0 ? actual.exitCode : 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, check out the docs at https://lerna.js.org/docs/introduction
    `);
}

初始化了一个yargs实例,然后执行globalOptions,给yargs设置一些全局的options,返回yargs实例。对于yargs不熟悉的可以先看本章下一个模块对yargs的讲解。 当我们输入的命令是ls时,就会进入到注册的listCmd中,执行对应的handler:

exports.handler = function handler(argv) {
  return require(".")(argv);
};
// 实际执行的是同目录index.js导出的函数
function factory(argv) {
  return new ListCommand(argv);
}

返回了一个ListCommand实例,我们看对应的类实现

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

  initialize() {
    ...
  }

  execute() {
    ...
  }
}

可以看到ListCommand并没有构造函数,那么肯定会执行父类的构造方法,我们看父类Command的构造方法:

class Command {
  constructor(_argv, { skipValidations } = { skipValidations: false }) {
    log.pause();
    log.heading = "lerna";

    const argv = cloneDeep(_argv);
    log.silly("argv", argv);

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

    // 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());
      // For the special "repair" command we want to intitialize everything but don't want to run validations as that will end up becoming cyclical
      if (!skipValidations) {
        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
    // argv添加一些属性
    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方法:

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方法,然后执行execute方法,由于子类实现了这两个方法,所以会执行ListCommand中对应的方法,到此流程结束。PS:这里只是带着梳理一下命令的执行流程,具体的实现可以自行探索。

yargs

Yargs的官方介绍是:可以帮助你构建交互式命令行工具,通过解析参数和生成优雅的用户界面。其实这个工具就是帮助我们更加方便的注册命令、解析参数、设置全局options或者命令options、设置别名等

基本使用

const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')
const pkg = require("../package.json");
const context = {
  frontVersion: pkg.version,
};
// 参数解析
const argv = hideBin(process.argv)
// yargs实例
const cli = yargs()
cli
  .usage('Usage: $0 <command> [options]') // 脚手架命令使用格式
  .demandCommand(1, 'A command is required. Pass --help to see all available commands and options.') // 最少参数个数
  .strict()
  .recommendCommands() // 命令纠正提示
  .fail((err, msg) => { // 自定义命令纠正提示
    console.log(err)
  })
  .alias("h", "help") // 设置别名
  .alias("v", "version")
  .wrap(cli.terminalWidth()) // 控制台打印信息换行宽度
  .parse(argv, context); // 向argv注入context

这时我们输入脚手架的-h命令,就会在控制台打印如下信息

脚手架(二):lerna

设置options

设置options有两种方式,批量/单个设置:

cli
  .options({ // 批量定义全局选项 通过--help查看
    debug: { // 命令名称
      type: "boolean", // 命令值类型
      describe: "Bootstrap debug mode", // 命令描述
      alias: "d" // 命令别名
    },
    registry: {
      type: "string",
      describe: "Define global registry",
      alias: "r"
    }
  })
  .option("ci", { // 单个定义option
    type: "boolean",
    hidden: true,
  })

对于所有options还可以使用group进行分组:

cli
  .group(["debug", "registry", "help", 'version'], "Global Options:") // 命令分组

这样在使用-h查看options可以自定义options输入哪个分组 设置完执行-h执行打印如下所示:

脚手架(二):lerna

注册命令

注册命令使用command方法,该方法的传参有两种方式,lerna使用的是传入对象

cli
  .command( // 注册命令
    'init [name]', // 命令名称
    'Do init a project', // 命令描述
    yargs => { // builder 命令执行前执行
      yargs
        .option('name', { // 定义命令options
          type: 'string',
          describe: 'Name of a project',
          alias: 'n'
        })
    },
    (argv) => { // handler 命令执行函数
      console.log(argv)
    }
  )
  .command({ // 另一种注册命令方式 传入对象
    command: 'list',
    aliases: ['ls', 'la', 'll'],
    describe: 'List local packages',
    builder: yargs => {},
    handler: argv => {
      console.log(argv)
    }
  })

以上就是yargs的使用方式。

转载自:https://juejin.cn/post/7230747155370393658
评论
请登录