看呐,妈妈,没有执行人。
project.json 里引用了 @nx/jest:jest、@nx/webpack:webpack 或类似的 executor,这次改动就与你有关。
这次变更是提前公告过的,而且从 Nx v23.0.0 起,每个受影响的 executor 都会打印弃用警告。不过,“早告诉过你了”并不能当成迁移方案。对大多数工作区而言,切换后最大的变化是要维护的配置更少了,同时还带来一些 executor 时代根本做不到的功能。本文将讲清楚 executor 的来龙去脉、为什么由 inferred tasks 取而代之,以及如何迁移整个工作区。
Executor 简史
Executor 几乎和 Nx 本身一样古老。它最初叫builders,是从 Angular 借来的概念,后来随着 Nx Devkit 的推出改名为 executors。它的定位一直没变:把特定工具所需的自定义行为封装进工作区,对外提供统一的契约。executor 从项目配置中接收一组选项,完成工具需要的事情,然后报告成功或失败。正因为所有任务都走同一套契约,Nx 才能在不了解内部工具细节的情况下,收集日志、缓存任务、编排执行、分发到多台机器并支持重试。
但问题也恰恰出在这套契约上。executor 本质上是一个包装层,而包装层必须了解被包装对象的结构。底层工具的每个选项都需要在 executor schema 中有对应条目,上游每新增一个 flag,我们就得发一版;工具每次破坏性变更,executor 也得跟着改。这还导致了一个“双份真相”的问题:工具有自己的配置文件,executor 的选项又写在 project.json 里,两者必须保持一致。一旦不一致,通常是 executor 说了算,而配置文件就在不知不觉中成了摆设。
Inferred targets 的诞生
另一种方式由 Nx v18 和 Project Crystal 引入,关于技术细节,我们最初的博文依然是最佳参考。借助推断任务,插件不再去封装工具,而是读取工具已有的配置文件,并从中推断出 targets。项目中存在 vite.config.ts,就意味着该自动获得 build、serve 和 preview 这三个 targets;存在 jest.config.ts,则获得 test target;存在 eslint.config.ts,则获得 lint target。插件对这些工具足够熟悉,能够根据该配置推导出正确的 inputs、outputs 和 cache 设置,而 target 本身则通过 nx:run-commands 运行原生的 CLI。
最后这一点正是关键差异所在。你和工具之间不再有 executor 封装层。给 nx test my-app 传 --coverage 参数,就会直接把 --coverage 传给 Vitest。升级到 React 的新主版本,只需要修改 package.json 即可,无需等待下一个 Nx 版本发布。插件只是一层薄封装,这意味着它更不容易出错,也更容易维护。
这也让插件的侵入性大大降低。将其放到现有工作区上,它就能根据已有的配置推断出 targets。移除它之后,你依然可以直接调用该工具,因为工具的配置从未由 Nx 接管。你唯一需要维护的,只是那些偏离插件推断结果的部分。
推断任务还解锁了一些在 executor 模式下难以实现或不可能的功能:
- Atomizer。由于插件能看到所有测试文件,它可以为每个文件生成一个 target,并让 Nx 将它们在多台机器上分布执行。我们在 3 Test Splitting Techniques that Cut E2E Times up to 90% 中详细讨论过这一机制。
- 默认的缓存正确性。Inputs 和 outputs 是从实际配置推导出来的,而不是根据约定盲目猜测。
- 无需配置样板代码。只要项目带有工具相关配置,就完全不需要
project.json。 - 松耦合。只要配置文件格式保持可识别,插件和工具就可以各自独立演进。
被移除的内容
此次移除仅涉及核心 Nx 插件自带的那些执行器。执行器 API 本身依然保留。例如 nx:run-commands 就是一个执行器,所有推断式目标都会通过它运行。此外,@nx/js 的构建执行器(tsc、swc、node)、@nx/esbuild:esbuild、所有 @nx/angular 执行器,以及针对 Gradle 和 Maven 的批量执行器也均予以保留。你自己编写的执行器,或从社区插件安装的执行器,在 Nx v24 中将与现状保持一致并继续正常工作。执行器 API 是扩展点;内置执行器只是我们基于该点构建的第一个组件。
以下执行器已被弃用,并将在 Nx v24 中移除。它们各自都有对应的推断式插件版本,并配备了 convert-to-inferred 生成器。
Nx v24 中移除的执行器:
@nx/cypress:cypress@nx/detox:build、@nx/detox:test@nx/eslint:lint@nx/expo:build、export、install、prebuild、run、serve、start、submit@nx/jest:jest@nx/next:build、@nx/next:server@nx/playwright:playwright@nx/react-native:build-android、build-ios、bundle、pod-install、run-android、run-ios、start、upgrade@nx/remix:build、@nx/remix:serve@nx/rollup:rollup@nx/rspack:rspack、@nx/rspack:dev-server@nx/storybook:storybook、@nx/storybook:build@nx/vite:build、@nx/vite:dev-server、@nx/vite:preview-server@nx/vitest:test@nx/webpack:webpack、@nx/webpack:dev-server
@nx/angular、@nx/react 和 @nx/rspack 中的 Module Federation dev-server executor 也已被移除,但它们的迁移路径不同,详见 Nx v23 发布公告。
转换你的 workspace
转换器是一个 generator,从 Nx v19 起就已存在,所以你的 workspace 很可能已经运行过部分转换。如果还没有,整个过程分三步。
第一步,升级到最新的 Nx v23 版本。v23.2 及之后的转换 generator 比早期版本快得多,也能处理更多边界情况:
npx nx migrate latest
npx nx migrate --run-migrations第二步,执行转换。infer-targets generator 会找出所有带有 convert-to-inferred generator 的已安装插件,并逐一运行:
npx nx g infer-targets如果你想逐个插件或逐个项目进行,每个插件也提供了自己的 generator:
npx nx g @nx/eslint:convert-to-inferred --project my-app第三步,检查结果。generator 会在 nx.json 中注册插件,把它推断出的 target 与 project.json 中已有的 target 进行对比,只保留差异部分。插件已能从配置文件推导出的选项会被移除;仅针对你 target 的选项则会被移入工具的配置文件、作为命令行参数添加,或作为覆盖项留在 project.json 中。当 workspace 里所有项目都有相同的覆盖项时,generator 会把它提升到 targetDefaults,只定义一次。最后还会再做一次推断,验证生成的 target 与之前等价。
npx nx show project my-app --web项目详情页会展示每个 target 的来源以及最终使用的选项。target 旁边显示插件名的就是推断生成的;如果某个 target 仍列出 executor,说明它被跳过了,generator 的输出会告诉你原因。
检查useInferencePlugins如果你的 nx.json 中设置了 "useInferencePlugins": false,推断插件会在整个 workspace 中被禁用,转换也就不会产生任何效果。请在运行 generator 之前删除该配置项,或将其设为 true。
两种情况需要手动处理。通过 package.json 脚本定义的目标会被保留原样,因为生成器无法判断脚本是否意在作为唯一事实来源。仍在使用 composePlugins 和 withNx 的 Webpack 项目,需先将配置转换为 NxAppWebpackPlugin,Nx v23 发布公告中也对此有所介绍。转换指南和故障排查页面列出了我们已知的其他例外情况。
关于图计算
在使用执行器(executors)时,Nx 直接从 project.json 读取目标。在使用推断任务时,插件会从工具配置中推导目标,这意味着在计算项目图时需要多做一点工作。这些工作只在配置变更后发生一次。守护进程(daemon)会缓存结果,因此日常运行的命令均由缓存的图提供服务。
如果你在转换功能早期就试用过,可能记得当时的工作量较为显著。Nx v22 和 v23 的两个版本周期中,很大一部分工作投入到了插件、守护进程和转换生成器的优化上,目前剩余的问题已很少。
如果你的工作区恰好属于例外情况,我们希望听到你的反馈。运行 NX_PERF_LOGGING=true NX_DAEMON=false nx graph 可查看时间消耗在哪里,并将输出结果发给我们。高性能插件指南介绍了如何优化你自行编写的推断插件。
结论
执行器支撑了 Nx 过去近十年,它是那些根本不了解单体仓库(monorepo)概念的工具也能享受缓存和分发能力的原因。推断任务延续了这一约定,并去除了阻碍项,这也是 Nx v24 移除在 Nx v23 中已弃用的内置执行器的原因。
转换只需运行一个生成器,耗时几秒,且转换后的工作区配置比之前更少。在 Nx v23 上运行它,使用 nx show project 验证结果,即可在 Nx v24 正式发布前做好准备工作。如果转换过程与本文描述不符,请提一个 issue 或到 Discord 上找到我们,我们会共同排查。