02. Monorepo 与启动:pnpm、Turbo、DI 和 CLI
2.1 根目录不是应用,而是构建控制面
根 package.json 的 name 是 n8n-monorepo,版本为 2.33.0,包管理器固定为 pnpm 10.32.1,Node 要求 >=22.22。根脚本把复杂度交给 Turbo:
build:所有包按依赖拓扑构建。dev:并行启动后端、前端和开发包,并排除不适合热启动的包。test、typecheck、lint:按照 Turbo pipeline 运行。build:n8n、build:docker:把包构建结果聚合成可运行发行物。
根目录通过 pnpm-workspace.yaml 声明 workspace,通过各包的 package.json 维护边界。阅读时,先看包的 name、dependencies、exports 和 scripts,再看源码。
2.2 包的职责分区
关键包可以这样记:
| 包 | 主要问题 |
|---|---|
packages/workflow | 工作流是什么,数据如何表示,节点能看到什么 |
packages/core | 图如何执行,节点如何得到上下文,触发器如何管理 |
packages/cli | 服务器如何启动,API、认证、数据库、队列如何组合 |
packages/nodes-base | 官方集成如何以节点描述和执行函数出现 |
packages/frontend/editor-ui | 编辑器如何呈现图、参数和 execution 状态 |
packages/@n8n/db | Entity、Repository、迁移和数据库连接 |
packages/@n8n/task-runner* | 用户代码和外部任务的隔离执行 |
2.3 启动链路
CLI 命令的典型路径是:
packages/cli/bin/n8n
→ commands/start.ts
→ server.ts / abstract-server.ts
→ DI container 注册服务
→ 数据库、节点、凭证、Webhook、Push、controller 初始化
→ HTTP server listen
@n8n/di 提供 @Service() 等装配基础。服务类把依赖写在构造函数里,由容器在启动阶段解析。这种构造函数注入让 WorkflowRunner、CredentialsHelper、ExecutionService 等大型服务可被单元测试替换依赖;代价是启动图很大,新增服务时必须理解模块加载顺序和生命周期。
相关源码:
packages/cli/src/commands/start.tspackages/cli/src/server.tspackages/cli/src/abstract-server.tspackages/@n8n/di
2.4 启动后的三类注册
启动阶段不只是监听端口,还要完成三类注册:
- 节点注册:扫描
nodes-base、LangChain 和社区包,建立 node type → class/description 的索引。 - 路由注册:controller registry 将控制器的方法挂到 REST、Webhook、OAuth、MCP 等路由。
- 运行时注册:激活工作流的 trigger、poller、scheduler,worker 的队列消费者,以及 Push/SSE 通道。
这解释了为什么“只启动一个 HTTP server”不能还原 n8n:真正可工作的实例还需要数据库、节点加载器、密钥配置和运行时注册表。
2.5 设计取舍
Turbo + workspace 解决的是源码规模,不是运行时隔离。包在源码层分离,最终仍可能被 n8n CLI 聚合进同一个 Node 进程。DI 解决的是依赖可替换性,不会自动解决模块边界;边界仍由 package dependencies、ESLint/boundaries 检查和代码审查维护。
开发模式为什么要过滤包? 前端、后端、设计系统、task runner 的 watch 模式对端口、资源和依赖要求不同。根脚本通过 --filter 把常用开发面收窄,避免一次启动整个 monorepo。
2.6 读源码技巧
先从 package graph 画出“谁能依赖谁”,再从 commands/start.ts 反向追服务实例。遇到一个服务时,先看构造函数依赖和 init/shutdown 方法,再看业务方法;这比直接从几千行 service 中间跳转更容易恢复生命周期。