使用 Liftoff 在 Node 中构建命令行工具 - 文章教程

使用 Liftoff 在 Node 中构建命令行工具

发布于 2021-08-07 字数 9269 浏览 935 评论 0

在 node 中编程我最喜欢的事情之一是包管理系统。在几乎所有情况下,为每个项目在本地安装模块的做法简化了我作为开发人员的生活。

然而,作为 Grunt 的长期贡献者,我已经非常熟悉这种实践失败的一个边缘情况。为了减轻大家的烦恼,我创建了一个库来解决这个问题。它被称为Liftoff

如果您曾经为 node 构建过命令行工具,尤其是使用插件生态系统的命令行工具,那么您可能知道我在说什么。让我们来看看 Liftoff 旨在解决的问题。

全局模块语义

在我深入探讨之前,让我解释一些事情,以确保我们都在同一页面上:

  1. 如果某个模块提供了您想在 shell 中使用的命令,则使其在系统范围内可用的最简单方法是全局安装它 ( npm install -g modulename)。
  2. 如果不使用 nvm 之类的工具,您不能在全局范围内安装多个版本的模块。
  3. 全局安装的模块不能使用require其他全局安装的模块。
  4. 本地安装的模块不能require全局安装的模块。

注意:所有模块都可以访问全局 核心模块,例如fspathhttp

这意味着什么

上述语义产生了一些不直观的皱纹:

  1. 基于节点的命令行工具通常会在全局和本地安装。
  2. 全局安装版本的作用应该是提供一个查找和加载本地版本的命令。
  3. 如果该工具使用插件,则本地安装的版本将无法访问任何已全局安装的版本。插件应指定为每个项目的依赖项。

如果您不熟悉 node,其中一些可能看起来很烦人。我的建议?只是滚动它。如果有足够的时间,您将完成使用多个版本的工具和插件的项目。

获得全球与本地的权利

为便于讨论,让我们构建一个名为 hacker 的命令行工具,并使用 Hackerfile. 当我们 hacker 在 shell 中运行命令时,我们希望它从我们全局安装的版本(如我们工具的package.json bin 属性中指定)执行二进制文件。如前所述,该命令应该查找并加载我们工具的本地安装,无论它是什么版本。

因为全局安装的模块不能本地安装require,所以需要一些黑客来完成这项工作。幸运的是,我们需要的算法已经在 npm 上可用。它被称为resolve,它通过几个方便的可配置选项复制 了 node 查找模块的 方式,包括指定基本目录以开始搜索的能力。

hacker到目前为止,这是我们二进制文件的一个简单示例:

#!/usr/bin/env node
var resolve = require('resolve');
try {
  var localHacker = resolve.sync('hacker', { basedir: process.cwd() });
  console.log('Found hacker at', localHacker);
  // kick off here
} catch (e) {
  console.log('Unable to find a local installation of hacker.');
  process.exit(1);
}

让它运作良好

所以,现在我们可以找到并运行我们工具的本地版本!我们差不多完成了,对吧?不完全的。有许多细节需要添加。现在让我们检查其中的一些。

智能遍历

当我们运行 hacker 命令时,我们可能位于项目的子文件夹中。如果我们的工具足够智能,可以遍历文件系统以 Hackerfile 在最近的祖先目录中查找 a ,那就太好了。再一次,npm 有一个模块——它被称为 findup-sync,这就是我们可以使用它的方式:

#!/usr/bin/env node
var resolve = require('resolve');
var findup = require('findup-sync');
var path = require('path');
var cwd = process.cwd();

var configFile = findup('Hackerfile.js', { cwd: cwd });
if (configFile) {
  console.log('Found Hackerfile:', configFile);
  cwd = path.dirname(configFile);
  process.chdir(cwd);
  console.log('Setting current working directory:', cwd);
} else {
  console.log('No Hackerfile found.');
  process.exit(1);
}

try {
  var localModule = resolve.sync('hacker', { basedir: cwd });
  if (localModule) {
    console.log('Found hacker module:', localModule);
  }
  // kick off here
} catch (e) {
  console.log('Unable to find a local installation of hacker.');
  process.exit(1);
}

请注意,一旦找到 a Hackerfile,我们将进程的工作目录更改为它所在的文件夹。通过这样做,我们使用我们的工具执行的任何文件操作都将相对于我们的Hackerfile.

显式目录规范

最终,我们可能需要hacker从完全在我们项目之外的目录中运行。为了支持这一点,我们需要开始读取命令行标志。有很多很棒的选项解析器:optimistminimistyargsnomnomnoptcommander.js浮现在脑海中。基本上,我们不需要再建一个!

这是我们的二进制文件支持--cwd标志的样子:

#!/usr/bin/env node
var resolve = require('resolve');
var findup = require('findup-sync');
var path = require('path');
var argv = require('minimist')(process.argv.slice(2));

var cwd = argv.cwd ? argv.cwd : process.cwd();

var configFile = findup('Hackerfile.js', { cwd: cwd });
if (configFile) {
  console.log('Found Hackerfile:', configFile);
  cwd = path.dirname(configFile);
  process.chdir(cwd);
  console.log('Setting current working directory:', cwd);
} else {
  console.log('No Hackerfile found.');
  process.exit(1);
}

try {
  var localModule = resolve.sync('hacker', { basedir: cwd });
  if (localModule) {
    console.log('Found hacker module:', localModule);
  }
  // kick off here
} catch (e) {
  console.log('Unable to find a local installation of hacker.');
  process.exit(1);
}

支持 JS 变体进行配置

如果我们的工具被广泛采用,那么总会有人想用Hackerfile我们不使用或不关心的 JS 变体来编写他们的文件。明确地将它与我们的工具捆绑在一起是个坏主意,所以我们需要支持另一个选项。让我们称之为--require。我们找到了选项解析器并学会了如何使用resolve是一件好事——我们需要找到更多的本地模块!

#!/usr/bin/env node
var resolve = require('resolve');
var findup = require('findup-sync');
var path = require('path');
var argv = require('minimist')(process.argv.slice(2));

var cwd = argv.cwd ? argv.cwd : process.cwd();
var requires = argv.require;

if (requires) {
  if (!Array.isArray(requires)) {
    requires = [requires];
  }
  requires.forEach(function (module) {
    try {
      require(resolve.sync(module, { basedir: cwd }));
      console.log('Loading external module:', module);
    } catch (e) {
      console.log('Unable to load:', module, e);
    }
  });
}

var validExtensions = Object.keys(require.extensions).join(',');
var configNameRegex = 'Hackerfile'+'{'+validExtensions+'}';

var configFile = findup(configNameRegex, { cwd: cwd });
if (configFile) {
  console.log('Found Hackerfile:', configFile);
  cwd = path.dirname(configFile);
  process.chdir(cwd);
  console.log('Setting current working directory:', cwd);
} else {
  console.log('No Hackerfile found.');
  process.exit(1);
}

try {
  var localModule = resolve.sync('hacker', { basedir: cwd });
  if (localModule) {
    console.log('Found hacker module:', localModule);
  }
  // kick off here
} catch (e) {
  console.log('Unable to find a local installation of hacker.');
  process.exit(1);
}

现在,而不是寻找Hackerfile一个.js扩展,我们正在寻找一个与任何扩展节点懂得如何加载。如果我们这样调用我们的命令: hacker --require coffee-script/register,我们的新二进制文件将尝试从我们的本地依赖项中要求咖啡脚本编译器。如果成功,节点将能够加载.coffee文件。

让这一切变得简单

所有这些以及一些都可以使用 Liftoff 自动化。下面的示例完成了我迄今为止描述的所有内容:

#!/usr/bin/env node
var Liftoff = require('liftoff');

var Hacker = new Liftoff({
  name: 'hacker'
}).on('require', function (name, module) {
  console.log('Loading external module:', name);
}).on('requireFail', function (name, err) {
  console.log('Unable to load:', name, err);
});

Hacker.launch(function() {
  if(this.configPath) {
    process.chdir(this.configBase);
    console.log('Setting current working directory:', this.configBase);
    // kick off here
  } else {
    console.log('No Hackerfile found.');
    process.exit(1);
  }
});

Grunt 的下一个主要版本(此处 正在开发)将由 Liftoff 提供支持。其他开源库已经在采用它。Gulp 今天在用,jscs 很快就 用上 了。我希望这个可重用的解决方案将帮助其他开发人员寻求制作更好的工具。

原文:https://bocoup.com/blog/building-command-line-tools-in-node-with-liftoff

如果你对这篇文章有疑问,欢迎到本站 社区 发帖提问或使用手Q扫描下方二维码加群参与讨论,获取更多帮助。

扫码加入群聊

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

目前还没有任何评论,快来抢沙发吧!

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

2512 文章
30 评论
83581 人气
更多

推荐作者

魏剑帆

文章 0 评论 0

yanggwq

文章 0 评论 0

qq_c2gI5

文章 0 评论 0

qq_iQVWB

文章 0 评论 0