年轻人的第一个 nx 插件


这是一系列博文中的一篇,用来记录将我将公司的前端项目改造为 monorepo 的过程。

1 / 4 of Nx Monorepo Experience

我们的项目中使用 lingui 作为 i18n 方案,在之前的实践中,每个项目都在 package.json 中维护了调用 lingui 提取 i18n 文本的命令,除此之外,在每个项目的 CI 中也包含了检查 i18n 文本是否存在遗漏的脚本。

把项目迁移到 nx 管理的 monorepo 后,就可以着手把各个项目中重复维护的 lingui 相关脚本交给 nx 插件来实现,减少维护 lingui 相关脚本的负担。

添加 lingui executor

lingui 并没有提供官方的 nx 插件,但我们自己在 monorepo 中创建一个 nx plugin 来实现相关的功能。一般来说,nx 插件能够提供三种能力:

  1. executors,用来执行工具命令,例如,编译、运行测试
  2. generators,用来创建模板文件
  3. 自动推断项目类型,可以根据项目类型自动地为项目添加相关的开发脚本

对于 lingui 插件来说,只需要用到其中的 executor,在项目的 project.json 中添加 lingui executor,就可以通过 nx lingui 来调用 lingui cli

借助 nx 提供的 nx-plugin 插件,可以很快的把实现这些功能的基本模板搭建起来。

实现 Lingui Executor

Lingui Executor 的内容非常简单,只需要调用 lingui cli 就行了。这里我推荐使用 zx 来执行命令,zx 开箱即用,帮你处理好了调用命令行程序的一切工作,包括参数解析、命令行输出捕获、glob 匹配。唯一需要注意的是,zx 目前只提供 ESM 模块,而 nx 暂时无法调用 ESM 模块插件。为了兼容这两者之间的模块格式,我们的插件只能编译为 CJS 模块,通过 import('zx') 来引用 zx 中的函数。方便起见,我封装了 zx 中最常用的 $ 函数,使之可以自然地在 CJS 中使用:

import { logger } from '@nrwl/devkit';
export async function $$(template: TemplateStringsArray, ...args: any) {  
 const { $ } = await import('zx');  
 try {
   // 强制终端彩色输出
   $.env.FORCE_COLOR = '1';  
   // 关闭命令回显
   $.verbose = false;  
   if ($$.cwd) {  
     $.cwd = $$.cwd;  
   }  
   const process = $(template, ...args);  
   process.stdout.on('data', (data) => {  
     logger.info(data.toString());  
   });  
   process.stderr.on('data', (data) => {  
     logger.info(data.toString());  
   });  
   return await process;  
 } catch (e) {  
   logger.error(e.stdout);  
   logger.error(e.stderr);  
   throw e;  
 }  
}  
  
$$.cwd = undefined as string | undefined;

需要注意的是,zx 执行命令的 stdio 必须通过 @nrwl/devkit 输出到终端,否则 nx 的缓存机制将无法取得 executor 执行过程中产生的命令行输出内容。

export default async function runExecutor(options: ILinguiExecutorSchema, context: ExecutorContext) {    
 if (!options.localesDir) {  
   logger.error('options.localesDir is required');  
   return { success: false };  
 }  
 const localesDirPath = path.resolve(context.root, options.localesDir);  
 if (!context.projectName) {  
   return {  
     success: true,  
   };  
 }  
 const projectCwd = await getProjectCwd(context);  
 $$.cwd = projectCwd;
 try {
   await $$`yarn exec lingui extract --clean`;
 } catch (e) {
   return { success: false };
 } 
 return {  
   success: true,  
 };  
}

编译插件

在运行 nx 命令时,nx 插件会被执行,所以需要在开发者调用任何 nx 命令之前就把本地项目中的插件编译成 nx 可以加载的 CJS 模块。我的做法是给本地插件加上一个 postinstall 脚本,这样在运行 yarn install 的时候,nx 插件就会被自动地编译,不需要手动干预。

nx 默认生成的模板会使用 TypeScript 将代码编译为 CJS 模块,但 tsc 的编译速度比较慢,测试下来稍微有点影响实际的开发体验,我们可以用 swc 作为 tsc 的替代品,编译速度会快上很多。

另一个用来编译插件的选项是 esbuild,但 esbuild 的目标并不是替代 tsc,在某些行为上跟 tsc 有不少差异,需要对项目做很多改动,才能适配 nx 执行 executor 的机制。对于 nx 插件来说,executor 跟 generator 都是插件模块的入口(entry),而 esbuild 是设计来将代码打包成一个 Bundle 的。

最后

给自己的项目开发 nx 插件是一件非常简单的事情,但是由于 nx 加载插件的限制,需要注意插件的编译方式以及 ESM 的引用方式。

Series Navigation拒绝重复劳动,自动为项目添加 target >>
,

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

You can use markdown syntax in comment