让我们创建一个节点CLI来从模板中生成文件!
#javascript #node #npm #cli

项目: cfft-从模板cli

创建文件

在本文中,我解释了为什么我创建了用于根据模板生成文件的CLI,我必须解决的问题以及我学到了什么。

您会发现有关构建CLI的许多有用信息:

  • 如何设置应用程序,
  • 如何构建彩色记录仪,
  • 如何读取命令行参数,
  • 如何使用CLI提出问题,
  • 如何创建配置文件,
  • 如何根据当前终端位置找到配置文件,
  • 如何检索软件包版本
  • 如何为CLI创建表格,
  • 如何将您的软件包发布到NPM。

关于CFFT

那么,什么是CFFT CLI?

这是一个简单但功能强大的节点CLI,可用于从模板中生成文件列表。它是一个文件生成器。无论框架如何,我们都可以使用它。

功能:
•使用终端创建自定义文件结构
搜索并替换 - 用自定义文本或文件名替换文件内容,
•创建多个模板
•将选项直接设置为 cli参数回答CLI问题或:
•SET 默认通过使用 .config json 文件进行每个模板的文件 - cfft.config.json 。。

有关更多信息,请参见npm软件包。

为什么我建造它?

我是一个完整的堆栈开发人员,专注于前端,目前正在从事基于React的项目。我还使用和/或理解其他框架和语言,例如node.js,nest.js,next.js,angular,.net等。例如,Angular具有一个很好的CLI,可以帮助您创建组件使用交互式命令行接口和其他文件。它为您节省了很多时间。我决定构建类似的东西,以便在我的React项目上使用,但不仅限制了这一点 - 我想创建独立于框架的东西。我构建了它是为了节省重复任务上花费的开发时间,因为我自己复制/粘贴并重复重命名相同的文件结构。

让我们构建应用程序!

注意:这不是整个应用程序。我只是在解释其中的一些有趣的部分。检查GitHub查看代码。

设置应用程序

我决定使用以下语言和软件包:

  • node.js这是一个节点CLI应用程序,因此我们需要在计算机上安装一个节点,
  • typescript用于类型安全,
  • esbuild是一个易于配置的捆绑包,
  • jest and ts-jest – for testing,
  • arg用于阅读命令行参数,
  • chalk用于彩色控制台日志,
  • cli-table用于在游戏机中编写表格,
  • inquirer提问。

开始开发,创建一个新项目,通过创建 package.json npm init命令)初始化新节点项目,并安装CLI所需的软件包。如果您想使用git,请使用git init命令初始化它。

下一步是创建启动(可执行)文件并捆绑应用程序。

在您的源目录中创建一个新文件:

/src/index.ts

const run = () => {
  console.log('Hello World');
};

run();

创建 esbuild.js 在根文件夹中以配置捆绑:

esbuild.js

require("esbuild")
  .build({
    entryPoints: ["src/index.ts"],
    bundle: true,
    outdir: "./dist",
    platform: "node",
    loader: { ".ts": "ts" },
    minify: process.env.NODE_ENV !== "development",
    external: ["fs", "path", "util"],
  })
  .catch(() => process.exit(1));
  • 入口点进入输入文件的路径(从哪里开始),
  • 捆绑包应该捆绑
  • OUTDIR
  • loader 我们告诉Bundler我们要使用Typescript,
  • 缩小通过使其较小的代码优化我们的代码
  • 外部 - 从捆绑包中排除什么(我们不需要 fs path util util 由于这些已经包含在节点中)。

市场上还有许多其他捆绑包,例如 webpack swc turbopack parcel ,但是我决定使用 esbuild ,因为很容易配置与Typescript一起使用。

记录器

日志对于任何命令行应用程序都非常重要。这是该应用程序与用户通信的方式。

我倾向于创建充满表情符号的彩色消息,以使控制台更有趣。例如:

using chalk - example

为了使我的日志着色,我使用了一个chalk软件包并创建了一个Logger实用程序:

/src/logger.ts

const log = (...message: any[]) => write(...message);

const success = (...message: any[]) => write(chalk.green(...message));

const warning = (...message: any[]) => write(chalk.yellow(...message));

const info = (...message: any[]) => write(chalk.blue(...message));

const error = (...message: any[]) => write(chalk.red(...message));

const debug = (...message: any[]) =>
  isDebug() ? write(chalk.whiteBright("🔧 [DEBUG]", ...message)) : undefined;

const write = (...message: any[]): void => {
  const shouldWrite = process.env.APP_ENV !== "test";

  if (shouldWrite) {
    console.log(...message);
  }
};

export default { success, warning, info, error, log, debug };

读取命令行参数

阅读CLI参数也是每个CLI的重要组成部分。这是为应用程序提供其他选项的一种方法。例如:

cfft --template css --fileName my-styles

在这里,我们告诉我们的CLI使用 css 模板并创建 myStyles 文件结构。

可以在不安装任何其他软件包的情况下提取命令行参数:

var arguments = process.argv;

console.log(arguments);

,但我决定安装arg软件包。这是一个不错的实用程序,它将命令行参数存储在对象中,允许您指定别名,为您进行验证以及更多。

要指示允许的选项,类型和别名,您需要创建一个对象并将其传递给ARG函数:

import arg from "arg";

export const CLI_ARGS_TYPE = {
  "--fileName": String,
  "--dirPath": String,
  "--template": String,
  "--templatePath": String,
  "--shouldReplaceFileName": String,
  "--fileNameTextToBeReplaced": String,
  "--shouldReplaceFileContent": String,
  "--textToBeReplaced": String,
  "--replaceTextWith": String,
  "--searchAndReplaceSeparator": String,
  "--debug": String,
  "--version": Boolean,
  "--help": Boolean,

  // Aliases
  "-n": "--fileName",
  "-t": "--template",
  "-v": "--version",
};

console.log(arg(CLI_ARGS_TYPE));

如果运行以下命令:

cfft -n MyFile --template MyTemplate

结果将是:

{ _: [], '--fileName': 'MyFile', '--template': 'MyTemplate' }

注意:有关更多信息,请参见GitHub上的/src/options文件夹。

通过问问题填写丢失的选项

我们的CLI应该对用户友好。如果我们的用户忘记了指定一些选项,我们应该要求他这样做。

我们可以使用inquirer软件包轻松实现这一目标。此软件包允许您提出不同类型的问题,例如输入问题(开放文本问题),确认问题(是/否)等。

例如,提出一个简单的输入问题可以像这样写:

import inquirer from "inquirer";

(async () => {
  const answer = await inquirer.prompt([
    {
      type: "input",
      name: "fileName",
      message: "Enter file name:",
    },
  ]);

  console.log(answer);
})();

结果:
inquirer example result

对于某些问题,您也可以设置默认值:

import inquirer from "inquirer";

(async () => {
  const answer = await inquirer.prompt([
    {
      type: "input",
      name: "fileName",
      message: "Enter file name:",
      default: "MyFile",
    },
  ]);

  console.log(answer);
})();

结果:
inquirer with default value example result

注意:有关更多信息,请参见GitHub上的/src/questions.ts/src/options

创建一个配置文件

拥有一个配置文件,用户可以在其中设置一些默认值对于CLI应用程序非常有用。您以前可以看到,在撰写本文时,CFFT CLI有10个以上不同的选择。我们的用户可能会忘记填充它们,或者他们每次都想为其模板使用相同的值。为了允许他们指定这些值,我们需要一个配置文件。

我在实施此功能时面临两个问题:

  1. 如何知道配置在哪里位于项目的根源?
  2. 如何知道它是否存在?

当我有上述问题时,我通常将其分为多个步骤:

  1. 如何知道当前终端路径?
  2. 如何知道配置文件是在父目录中还是在祖父母目录中,等等?
  3. 如何知道何时停止搜索?
从逻辑上讲,

cfft从当前终端路径上的模板创建文件。要知道该路径,您可以使用以下node.js函数:

process.cwd()

要检查我们的文件是否存在于当前路径上,我们应该尝试阅读它。如果存在,那是成功的,如果没有,该函数将引发异常,因此,在这种情况下,我们在父文件夹中检查。

清楚地搜索父目录,我们需要递归。

,最后,如果当前路径与以前的路径相同,则意味着我们到达了最后一个父目录,并且我们的文件不存在。

这是结合所解释的所有内容的函数:

export const findConfig = async (
  pathArg = ".",
  previousPath?: string
): Promise<Config> => {
  const searchPath = path.join(process.cwd(), pathArg, CONFIG_FILE_NAME);
  const folderPath = resolve(path.join(process.cwd(), pathArg));
  const currentPath = resolve(searchPath);

  if (currentPath === previousPath) return null as any;

  Logger.debug(`Searching for config. Path: ${currentPath}`);

  try {
    const file = await promisify(fs.readFile)(searchPath);

    Logger.debug(`Config file found: ${currentPath}`);

    const config = JSON.parse(file.toString()) as Config;
    config.folder = folderPath;
    config.path = currentPath;

    return config;
  } catch (error) {
    return await findConfig(path.join(pathArg, ".."), currentPath);
  }
};

注意:有关配置的更多信息,请参见GitHub上的/src/config文件夹。

检索软件包版本

向我们的用户展示当前的软件包版本可能非常有用。目的是在提供--version-v参数时显示它。这很容易实现:

require("./package.json").version

- -HELP命令

CLI的另一个非常有用且用户友好的命令是--help命令。此命令应帮助我们的用户使用我们的CLI。这是桌子的理想场所。我安装了cli-table包。

我决定显示三列:命令,别名和描述。

const table = new Table({
    head: ["Command", "Alias", "Description"],
    style: {
      head: new Array(3).fill("cyan"),
    },
  });

  let rows = [
    ["--fileName", "-n", "File name to be used"],
    ["--dirPath", "", "Path to the location where to generate files"],
    ["--template", "-t", "Name of the template to use"],
    ["--templatePath", "", "Path to the specific template folder"],
    ["—-shouldReplaceFileName", "", "Should or not CLI replace a file name",],
    ["--fileNameTextToBeReplaced", "", "Wich part of the file name should be replaced",],
    ["--shouldReplaceFileContent", "", "Should or not CLI replace a file content",],
    ["--textToBeReplaced", "", "Text to be replaced separated by a search and replace separator",],
    ["--replaceTextWith", "", "Text to be used for search and replace separated by a separator",],
    ["--searchAndReplaceSeparator", "", "Custom separator for search and replace"],
    ["--version", "-v", "Show the current version of the package"],
    ["--debug", "", "Show additional logs"],
  ];

  table.push(...rows);

  console.log(table.toString());

当我们执行help命令(cfft --help)时,我们应该看到以下结果(如果终端看起来不相同,则调整终端的大小):

The help command result

注意:检查/src/help.ts以获取有关GitHub的更多信息。

在本地测试您的代码

有了所有这些功能,我们的CLI几乎已经准备好了。但是在我们的发展过程中,在全球发布之前,我们想对其进行测试。

通过将以下行添加到package.json文件(请参阅package.json):
是可能的

"bin": {
    "cfft-dev": "dist/index.js"
 }

bin 是您添加全局命令的位置。我们基本上告诉Node,当我们执行cfft-dev命令时,它应该运行dist/index.js文件(这是我们在应用程序构建上创建的文件)。

非常重要的步骤是将以下代码行添加到您的可执行文件( index.ts )为第一行:

#!/usr/bin/env node

此行告诉节点这是一个节点可执行文件。

下一步是在本地安装命令。导航终端到您的软件包的位置。json,并执行以下命令:

npm link

瞧!您现在可以在本地测试您的CLI:

cfft-dev --version

发布到NPM

最后,但并非最不重要!我们的CLI完成了,我们希望允许其他人使用它。让我们发布到NPM。

/dist 文件夹中创建另一个package.json。填写所有必要的字段 - 作者 repository ,add 关键字版本,描述等。请参阅package.json < /p>

,但是,添加bin命令非常重要

"bin": {
    "cfft": "index.js"
 }

这样做,我们的CLI准备出版。导航到您的/dist 文件夹,执行npm publish并为自己感到骄傲!恭喜! ðªð(注意:关注npm上的文档)。


谢谢! ð