← 文章 / 未分类
nx 2小时前 · 2026-09-07 03:01:32 · 2 阅读

探索使用 Nx、TanStack 和 Rust 的多语言 monorepos

本文是 Nx 多语言 monorepo 系列的一部分:

多语言 monorepo(即包含多种不同技术栈的 monorepo)将越来越普遍。主要驱动力来自 agentic development(智能体开发),我最近观察到两个主要趋势:

  • AI 智能体在 monorepo 中大放异彩。这也是为什么越来越多的人开始考虑迁移的原因。一个典型例子:将前端和后端整合进同一个 monorepo
  • 学习新语言的门槛大幅降低。AI 智能体能够承担大量的维护开销和低层编码工作。

Nx 早已支持多语言:包括 .NETJavaRust 以及 Python。我近期会陆续推出这些语言的专题文章,但本篇将聚焦于 Rust。

不过我希望更进一步,看看能让智能体做到什么程度。于是我让 Codex 完成了以下任务:

  • 基于一个已有的 PNPM workspace monorepo(其中包含 TanStack Start 应用)
  • 将其改造为 Nx monorepo
  • 将 Rust 加入技术栈,具体做法是引入一个基于 Topcoat 的 Web 应用
  • 在 JavaScript Web 应用和 Rust 部分之间共享代码
  • 使用 Nx Cloud 和 Nx Agents 配置 CI

本文逐步讲解设置过程(也可以观看链接中的视频)。完整的 workspace 也已上传至 GitHub:juristr/nx-polyglot-rust,你可以直接让智能体指向它进行探索。

现有的 pnpm monorepo

起点是一个 PNPM workspace,这是以 JS 为基础构建 monorepo 的常见方式。

TanStack Start 应用位于 apps/web,各类包则放在 packages/ 目录下。

packages:
  - 'apps/*'
  - 'packages/*'
  - 'packages/*/*'

我还在此基础上添加了 Nx(只需运行 nx init)。为什么加入 Nx 很重要? Nx 提供了一个通用任务运行器,能够同时运行 JavaScript 和 Rust 目标。PNPM 适用于 JS 工作区,因此能触发 package.json 脚本,但不一定能触发 Cargo 别名。虽然我们可以强行让 PNPM 去触发那些命令,但我更不想这么做。

在workspace中添加Rust

那么我们如何将 Rust 安装到 workspace 中呢?我选择了 mise,我已经用了一段时间来管理各种工具的安装和版本控制。

mise 在这里的作用是通过驱动 rustup 来在 workspace 中安装 Rust 工具链,从而引入 Cargo、rustc 和 clippy,并使用定义的固定版本。优点在于:稍后在 CI 中也使用 mise,我可以避免各种本地与 CI 之间的版本冲突。

Rust monorepo 中包是如何链接的

Rust 有 Cargo(可以类比为 JS 世界中的 PNPM + package.json 脚本)。Cargo 有自己的 workspace 概念。根目录下的 Cargo.toml 列出了成员并集中管理共享依赖版本,包括本地 crate 之间的路径依赖(Rust 中的包):

Cargo.toml
[workspace]
resolver = "2"
members = [
  "apps/topcoat-security",
  "packages/shared/security-contract",
  "packages/topcoat/security-domain",
  "packages/topcoat/security-store",
  "packages/topcoat/security-ui",
]

[workspace.dependencies]
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "sync", "time"] }
topcoat = { version = "0.5.0", features = ["sse"] }
topcoat-security-domain = { path = "packages/topcoat/security-domain" }
topcoat-security-store = { path = "packages/topcoat/security-store" }

各个 crate 随后通过 .workspace = true 来使用这些条目:

packages/topcoat/security-store/Cargo.toml
[dependencies]
topcoat.workspace = true
topcoat-security-domain.workspace = true

这对应于 pnpm 端的 workspace:*

注意,Nx 并不参与其中的任何环节,就像 PNPM 为 JS 包做链接一样,Cargo 负责解析和链接 crates。

教会 Nx 运行 Rust 目标

Rust 支持来自 @monodon/rust 插件,注册在 nx.json 中。

运行以下命令即可将它添加到你的 workspace:

nx add @monodon/rust

之后你会看到插件已注册到 nx.json 中:

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  ...
  "plugins": [
    "@monodon/rust"
  ]
}

这个插件相当于一个适配器,把 Cargo 的操作暴露给 Nx,让 Nx 能把它们识别为可运行的目标——就像 Nx 自动检测 package.json 中的 script 目标一样。

因此,运行 nx build <rust-project> 会触发 cargo buildtest 对应 cargo testlint 对应 cargo clippyrun 对应 cargo run

什么是 Nx 插件?

Nx 插件是一个让 Nx 能理解原本不认识的工具的包。它读取该工具已经在用的配置文件(比如 Cargo.tomlpom.xmlvite.config.ts),并从中推导出项目、目标和依赖关系,这样缓存和 affected 功能就能直接生效,无需你手动编写这些接线代码。

需要注意:插件是可选的。任何包含 project.json 的文件夹本身就是一个 Nx 项目。所以你也可以手动给 Rust 项目添加 project.json 并定义要运行的目标,例如:

  {
    "name": "some-rust-package",
    "sourceRoot": "packages/.../src",
    "targets": {
      "build": {
        "command": "cargo build -p some-rust-package",
        "options": { "cwd": "{workspaceRoot}" }
      }
    }
  }

插件只是提升开发体验的一种方式。

想了解已有插件,可以查阅 Nx pluginsmulti-language supportplugin registry

JavaScript 项目通过 package.json 中的 scripts 声明任务,Nx 会直接识别。而 Rust crate 没有 package.json,所以要在 project.json 中声明目标:

apps/topcoat-security/project.json
{
  "name": "topcoat-security",
  "projectType": "application",
  "tags": ["scope:topcoat", "type:app", "lang:rust"],
  "targets": {
    "build": {
      "executor": "@monodon/rust:build",
      "outputs": ["{options.target-dir}"],
      "options": { "target-dir": "dist/target/topcoat-security/build" }
    },
    "test": {
      "executor": "@monodon/rust:test",
      "outputs": ["{options.target-dir}"],
      "options": { "target-dir": "dist/target/topcoat-security/test" }
    },
    "lint": {
      "executor": "@monodon/rust:lint",
      "outputs": ["{options.target-dir}"],
      "options": { "target-dir": "dist/target/topcoat-security/lint" }
    },
    "run": {
      "continuous": true,
      "executor": "@monodon/rust:run",
      "outputs": ["{options.target-dir}"],
      "options": { "target-dir": "dist/target/topcoat-security/run" }
    }
  }
}

"executor": "@monodon/rust:build" 正是任务委托给底层 @monodon/rust 插件的 build 目标的入口。

两套技术栈采用一致的命令执行方式

我个人非常喜欢这个方案的一点是,只需输入以下命令,Nx 就会在整个代码库中自动运行所有任务:

pnpm nx run-many -t build lint test typecheck

这意味着 Cargo、Vite、tsc、ESLint 等工具都能按照正确的依赖顺序执行,并且包含缓存机制

跨两套技术栈的任务流水线

Nx 在这里并不关心底层调用的是哪个工具。在任务层面,它们都是一样的,因此现有的概念如定义任务流水线也能直接使用。

例如,这里我们定义:每当启动 TanStack Start 应用(通过 pnpm nx dev web),都要自动同时启动 Rust Topcoat 前端应用(topcoat-security):

apps/web/package.json
{
  "nx": {
    "targets": {
      "dev": {
        "continuous": true,
        "dependsOn": [{ "projects": ["topcoat-security"], "target": "run" }]
      }
    }
  }
}

这将转化为如下任务流水线:

web:dev
  -> topcoat-security:run (continuous)
       -> security-globe:vite:build

Nx terminal output for web:dev, showing topcoat-security:run and web:dev both marked Continuous, above the three dependency builds Nx restored from cache

Nx 对 Rust 的缓存

Nx run-many output with every task reporting existing outputs match the cache, mixing Rust tasks like topcoat-security-store:build with JavaScript ones like dashboard-ui:test, and a summary line reading 23 out of 23 tasks read from cache in 354ms

在 Nx 中,Rust 目标的缓存功能开箱即用。我们只需定义哪些内容需要缓存,以及输入和输出分别是什么。

nx.json
{
  "namedInputs": {
    "rust": [
      "default",
      "{workspaceRoot}/Cargo.toml",
      "{workspaceRoot}/Cargo.lock",
      "{workspaceRoot}/rust-toolchain.toml",
      "{workspaceRoot}/.mise.toml",
      "{workspaceRoot}/pnpm-lock.yaml"
    ]
  },
  "targetDefaults": {
    "@monodon/rust:build": {
      "cache": true,
      "inputs": ["rust", "^rust"],
      "dependsOn": ["^build"]
    },
    "@monodon/rust:test": {
      "cache": true,
      "inputs": ["rust", "^rust"],
      "dependsOn": ["^build"]
    },
    "@monodon/rust:lint": { "cache": true, "inputs": ["rust", "^rust"] }
  }
}

另一个与 Rust 相关的细节是上述 project.json 中各目标的 target-dir。Cargo 默认会将编译产物写入同一个共享的 target/ 目录。如果 buildtestlintrun 都指向同一目录,它们的产物会相互覆盖。为每个目标在 dist/target/<project>/<target> 下分配独立目录即可解决该问题。

CI on Nx Cloud

不过,配置 monorepo 不应止步于本地设置。因此我让 Codex 配置 monorepo,并同时告知它配置 Nx Cloud 作为 CI 基础设施,包括使用 Nx Agents。顺便说一句,这部分并不与 Rust 相关,CI 配置指南适用于任意 Nx 工作区。

最终效果如下:

.g
原始来源: nx

评论 (0)