脚本任务执行器 —— npm-run-all 源码解析

2022-12-05 09:02:05 浏览数 (1)

大家好,我是码农小余。很久不见,怪是想念。

最近在整一个 OpenAPI 编排器,想到 npm-run-all 的任务流。看了一下这个 6 年前的源码。npm-run-all[1] 是一个用来并行或者串行运行多个 npm 脚本的 CLI 工具。阅读完本文,你能收获到:

  • 了解整个流程概览;
  • 了解核心模块逻辑,入口分析、参数解析、任务流、任务执行等;

流程概览

直入主题,整个 npm-run-all 的整体执行流程如下:

当我们在终端敲入命令,实际上会去调用 bin/xx/index.js 函数,然后调用 bootstrap 去分发不同命令不同参数的逻辑。help 和 version 比较简单,本文不做分析。任务控制方面,会先调用 npmRunAll 做参数解析,然后执行 runTasks 执行任务组中任务,全部任务执行后返回结果,结束整个流程。

入口分析

npm-run-all 包支持三条命令,我们看到源码根目录的 package.json 文件:

代码语言:javascript复制
{
  "name": "npm-run-all",
  "version": "4.1.5",
  "description": "A CLI tool to run multiple npm-scripts in parallel or sequential.",
  "bin": {
    "run-p": "bin/run-p/index.js",
    "run-s": "bin/run-s/index.js",
    "npm-run-all": "bin/npm-run-all/index.js"
  },
  "main": "lib/index.js",
  "files": [
    "bin",
    "lib",
    "docs"
  ],
  "engines": {
    "node": ">= 4"
  }
}

bin 下面定义的命令脚本:

  • run-p,简化使用的脚本,代表并行执行脚本;
  • run-s,简化使用的脚本,代表串行执行脚本;
  • npm-run-all,复杂命令,通过 --serial 和 --parallel 参数实现前两者一样的效果。

直接看到 bin/npm-run-all/index.js

代码语言:javascript复制
require("../common/bootstrap")("npm-run-all")

上述代码中,如果是执行 run-p 这条命令,则函数传入的参数是 run-prun-s 同理。bootstrap 通过参数的不同,将任务分发到 bin 下不同目录中:

代码语言:javascript复制
.
├── common
│   ├── bootstrap.js
│   ├── parse-cli-args.js
│   └── version.js
├── npm-run-all
│   ├── help.js
│   ├── index.js
│   └── main.js
├── run-p
│   ├── help.js
│   ├── index.js
│   └── main.js
└── run-s
    ├── help.js
    ├── index.js
    └── main.js

照着上述代码结构,结合 ../common/bootstrap 代码:

代码语言:javascript复制
"use strict"

module.exports = function bootstrap(name) {
    const argv = process.argv.slice(2)

    switch (argv[0]) {
        case undefined:
        case "-h":
        case "--help":
            return require(`../${name}/help`)(process.stdout)

        case "-v":
        case "--version":
            return require("./version")(process.stdout)

        default:
            // https://github.com/mysticatea/npm-run-all/issues/105
            // Avoid MaxListenersExceededWarnings.
            process.stdout.setMaxListeners(0)
            process.stderr.setMaxListeners(0)
            process.stdin.setMaxListeners(0)

            // Main
            return require(`../${name}/main`)(
                argv,
                process.stdout,
                process.stderr
            ).then(
                () => {
                    // I'm not sure why, but maybe the process never exits
                    // on Git Bash (MINGW64)
                    process.exit(0)
                },
                () => {
                    process.exit(1)
                }
            )
    }
}

bootstrap 函数依据调用的命令调用不同目录下的 help、version 或者调用 main 函数,达到了差异消除的效果。

然后再把目光放在 process.stdout.setMaxListeners(0) 这是啥玩意?打开 issue 链接[2],通过报错信息和翻阅官方文档:

By default EventEmitters will print a warning if more than 10 listeners are added for a particular event. This is a useful default that helps finding memory leaks. The emitter.setMaxListeners() method allows the limit to be modified for this specific EventEmitter instance. The value can be set to Infinity (or 0) to indicate an unlimited number of listeners. 默认情况下,如果为特定事件添加了超过 10 个侦听器,EventEmitters 将发出警告。这是一个有用的默认值,有助于发现内存泄漏。emitter.setMaxListeners() 方法允许为这个特定的 EventEmitter 实例修改限制。该值可以设置为 Infinity(或 0)以指示无限数量的侦听器。

为什么要处理这个情况呢?因为用户可能这么使用:

代码语言:javascript复制
$ run-p a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11

你永远想象不到用户会怎么使用你的工具!

参数解析

分析完不同命令的控制逻辑,我们进入核心的 npmRunAll 函数,参数解析部分逻辑如下:

代码语言:javascript复制
module.exports = function npmRunAll(args, stdout, stderr) {
  try {
    const stdin = process.stdin
    const argv = parseCLIArgs(args)
  } catch () {
    // ...
  }
}

解析处理所有标准输入流参数,最终生成并返回 ArgumentSet 实例 set。parseCLIArgsCore 只看控制任务流执行的参数:

代码语言:javascript复制
function addGroup(groups, initialValues) {
    groups.push(Object.assign(
        { parallel: false, patterns: [] },
        initialValues || {}
    ))
}

function parseCLIArgsCore(set, args) {
    LOOP:
    for (let i = 0; i < args.length;   i) {
        const arg = args[i]

        switch (arg) {
       // ...
            case "-s":
            case "--sequential":
            case "--serial":
                if (set.singleMode && arg === "-s") {
                    set.silent = true
                    break
                }
                if (set.singleMode) {
                    throw new Error(`Invalid Option: ${arg}`)
                }
                addGroup(set.groups)
                break

            case "-p":
            case "--parallel":
                if (set.singleMode) {
                    throw new Error(`Invalid Option: ${arg}`)
                }
                addGroup(set.groups, { parallel: true })
                break

            default: {
                // ...
                break
            }
        }
    }

    // ...
    return set
}

将任务都装到 groups 数组中,如果是并行任务(传了 -p--parallel 参数),就给任务加上 { parallel: true } 标记。默认是 { parallel: false },即串行任务。

执行任务组

在进入这一小节之前,我们就 npm-run-all 源码在 scripts 下加一条 debug 命令:

代码语言:javascript复制
$ "node ./bin/npm-run-all/index.js lint test"

解析完参数生成的 argv.groups 如下:

代码语言:javascript复制
[{
  paralles: false,
  patterns: ['lint', 'test']
}]

有了这个数组结果,我们再看任务执行流程会更加明朗。

代码语言:javascript复制
// bin/npm-run-all/main.js
module.exports = function npmRunAll(args, stdout, stderr) {
    try {
      // 省略解析参数
        // 执行任务
        const promise = argv.groups.reduce(
            (prev, group) => {
                // 分组中没有任务,直接返回 null
                if (group.patterns.length === 0) {
                    return prev
                }
                return prev.then(() => runAll(
                    group.patterns, // ['lint', 'test']
                    {
                        // ……
                       // 是否并行执行
                        parallel: group.parallel,
                       // 并行的最大数量
                        maxParallel: group.parallel ? argv.maxParallel : 1,
                        // 一个任务失败后继续执行其他任务
                       // ……
                        arguments: argv.rest,
                        // 这个


	

0 人点赞