# Cell 4.8.0 > https://cell.cellbang.com/zh 的全部页面合成一个文件。错误码:https://cell.cellbang.com/zh/errors/ # 介绍 Cell 是一个 **Serverless 优先**、**组件化**、**平台无关**的 TypeScript 应用框架。你只需编写基于依赖注入的普通类,Cell 负责把它们组装成全栈应用,将后端打包为单个文件,并把同一份代码交付到 Node.js 服务器、容器、单个可执行文件、Cloudflare Workers、AWS Lambda、阿里云函数计算、腾讯云云函数或 Vercel。 **Cell 4** 基于 Vite 8 重写了工具链,并把 Coding Agent 视为一等用户:每条命令都能输出 JSON,每个错误都带错误码和修复建议,每个项目都自带 `AGENTS.md` 和 MCP 服务。 ## 为什么选择 Cell - **一套代码,所有运行时**:构建时选择 preset:`node`、`cli`、`cloudflare`、`fc`、`scf`、`lambda`、`vercel`。业务代码不依赖任何云厂商 SDK,迁移平台无需改写。 - **单文件产物**:后端是一个内联全部依赖的 `index.mjs`;加 `--single` 把前端也嵌进去,加 `--compile` 直接得到可执行文件(Bun、Deno 或 Node.js SEA)。 - **面向 Agent 的工具链**:所有命令支持 `--json`,结构化的 `CellError` 错误码与修复提示,`cell check` 一次检查类型与 DI 装配,文档与依赖版本严格匹配,项目和开发服务器都提供 MCP 工具。 - **企业级架构**:依赖注入与 AOP(`@Component`、`@Autowired`、`@Value`、`@Aspect`)、MVC、RPC、声明式事务、安全认证、JWT 与 OAuth2。 - **全栈一体**:后端 MVC 搭配 React;开发时前后端共用一个端口,前端 HMR,后端热重载。 - **原生 AI 能力**:与厂商无关的对话模型、提示词与流式响应;Agent 与 [pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) coding agent 会话,工具直接调用你的服务。 - **零配置**:处处有合理默认值,只在需要时通过 `cell.yml`、`cell.config.ts` 或 `CELL_*` 环境变量覆盖。 - **轻量依赖**:工具链几乎只依赖 Vite;云部署器自行完成请求签名,不引入庞大的厂商 SDK。 ## 快速开始 环境要求:开发和构建需要 Node.js `^20.19.0 || >=22.12.0`(Vite 8 的要求);构建出的应用可在 Node.js 18 及以上、Bun 1.2 及以上和 Deno 2 上运行,CI 会用这些运行时逐一启动示例的构建产物。 ```bash npx @celljs/cli init backend my-app # 或 fullstack(React 前端 + 后端,经 RPC 调用) cd my-app npm install npx cell dev # http://127.0.0.1:3000,保存即热重载 ``` 一个完整的后端,只需要一个配置文件和一个类: ```yaml # cell.yml app: greeting: Hello from my-app ``` ```typescript // src/node/home-controller.ts import { Value } from '@celljs/core'; import { Controller, Get, Text } from '@celljs/mvc/lib/node'; @Controller() export class HomeController { @Value('app.greeting') protected readonly greeting: string; @Get() @Text() home(): string { return this.greeting; } } ``` 在进程内测试,无需占用端口: ```typescript import { createTestApp } from '@celljs/cli'; const app = await createTestApp({ root: process.cwd() }); const res = await app.fetch('/'); // 200 "Hello from my-app" await app.close(); ``` 常用命令: | 命令 | 作用 | | --- | --- | | `cell dev` | 前后端同一端口;前端 HMR、后端热重载,配置变化自动重启 | | `cell build` | 生产构建到 `.cell/output`(支持 `--single`、`--compile`、`--preset `) | | `cell preview` | 按目标平台的方式运行构建产物 | | `cell deploy` | 构建并通过 preset 的部署器发布;`--dry-run` 只输出部署计划 | | `cell check` | 一次完成配置、TypeScript 类型、装饰器设置与 DI 绑定检查 | | `cell info` | 查看 Cell 解析出的全部信息:preset、mode、组件、DI 模块、hooks、产物状态 | | `cell config get --why` | 某个配置项的最终值,以及设置它的每个文件和环境变量 | ## 随处部署 代码保持不变,由 preset 决定产物形态和部署方式。 | Preset | 产物 | 部署 | | --- | --- | --- | | `node`(默认) | Node.js HTTP 服务,单个 `index.mjs` | 任何能运行 Node.js 的环境:虚拟机、容器、PaaS | | `cli` | 基于 Cell DI 的命令行程序 | 以单文件或可执行文件分发 | | `cloudflare` | Worker + 静态资源 + `wrangler.json` | `cell deploy --preset cloudflare` | | `fc` | 阿里云函数计算(自定义运行时) | `cell deploy --preset fc` | | `scf` | 腾讯云云函数(Web 函数) | `cell deploy --preset scf` | | `lambda` | AWS Lambda(Function URL / API Gateway) | `cell deploy --preset lambda` | | `vercel` | Vercel Build Output API(`.vercel/output`) | `cell deploy --preset vercel` | ```bash cell build --single # 整个全栈应用就是一个文件:node index.mjs cell build --compile # 独立可执行文件,目标机器无需安装 Node.js cell deploy --preset cloudflare --dry-run ``` ## 为 Coding Agent 而设计 Cell 假定项目的下一位开发者可能是 AI Agent,因此让框架的每一处都能被 Agent 一次用对([设计原则](https://github.com/cellbang/cell/blob/main/docs/design/agent-first.md))。 - **一切皆可机读**:所有命令加 `--json` 只输出一条 JSON 结果;错误统一为 `CellError`,带稳定的 `code`、出错位置和 `hint`。 - **给上下文,而不是让它猜**:`cell init` 生成 `AGENTS.md`、Cell 技能和 `.mcp.json`;`cell agent sync` 让它们与已安装的版本保持一致;`cell docs ` 检索与依赖版本完全匹配的随包文档。 - **内置 MCP**:`cell mcp` 提供 `info`、`check`、`config_get`、`docs_search`、`build`、`deploy_plan` 工具;`cell dev` 在 `/_cell/mcp` 暴露运行中应用的路由、DI 绑定、最终配置、近期请求、日志与错误。 - **快速反馈**:`cell check` 在运行之前发现类型错误、缺失的装饰器设置和断开的 DI 绑定;`createTestApp()` 提供进程内集成测试。 ```jsonc // .mcp.json(Claude Code、Cursor、CodeBuddy 等) { "mcpServers": { "cell": { "command": "npx", "args": ["cell", "mcp"] } } } ``` ## 原生 AI 能力 ```typescript import { Component, Autowired } from '@celljs/core'; import { ChatModel, ProxyChatModel, PromptTemplate } from '@celljs/ai-core'; @Component() export class Assistant { @Autowired(ProxyChatModel) protected chatModel: ChatModel; @Autowired(PromptTemplate) protected promptTemplate: PromptTemplate; async ask(question: string): Promise { const prompt = await this.promptTemplate.create('Answer briefly: {question}', { variables: { question }, // pi:/,由 ProxyChatModel 路由到 @celljs/ai-pi chatOptions: { model: 'pi:anthropic/claude-sonnet-4-5' } }); const response = await this.chatModel.call(prompt); return response.result.output.content; } } ``` - **模型**:`@celljs/ai-pi` 通过 `@celljs/ai-core` 的 `ChatModel` 接口接入 20 多家模型服务商(Anthropic、OpenAI、Google、DeepSeek、Ollama 及其他 OpenAI 兼容端点),模型名写 `pi:/`。 - **Agent**:`@celljs/ai-agent` 构建可调用工具的 Agent,工具就是普通的 Cell 组件,事件通过 SSE 推送。 - **Coding Agent**:`@celljs/ai-coding-agent` 在你的应用中托管 pi coding agent 会话,由 DI 统一管理。 - **终端界面**:`@celljs/tui` 把 Agent 变成交互式命令行对话(见 [`examples/tui-app`](https://github.com/cellbang/cell/tree/main/examples/tui-app))。 ## 更多示例
依赖注入 ```typescript import { Component, Autowired, Value } from '@celljs/core'; @Component() export class A {} @Component() export class B { @Autowired() protected a: A; // 类对象注入 } @Component() export class C { @Value('foo') // 配置属性注入:'obj.xxx'、'arr[1]' 等 protected foo: string; } ```
数据库访问与声明式事务 ```typescript import { Controller, Get, Param, Delete, Put, Post, Body } from '@celljs/mvc/lib/node'; import { Transactional, OrmContext } from '@celljs/typeorm/lib/node'; import { User } from './entity'; @Controller('users') export class UserController { @Get() @Transactional({ readOnly: true }) list(): Promise { return OrmContext.getRepository(User).find(); } @Get(':id') @Transactional({ readOnly: true }) get(@Param('id') id: number): Promise { return OrmContext.getRepository(User).findOneBy({ id }); } @Post() @Transactional() create(@Body() user: User): Promise { return OrmContext.getRepository(User).save(user); } @Put() @Transactional() async modify(@Body() user: User): Promise { await OrmContext.getRepository(User).update(user.id, user); } @Delete(':id') @Transactional() async remove(@Param('id') id: number): Promise { await OrmContext.getRepository(User).delete(id); } } ```
前端、后端与服务之间的 RPC ```typescript // 共享接口 export const UserService = 'UserService'; export interface UserService { hello(name: string): Promise; } // 服务提供方 import { Rpc } from '@celljs/rpc'; @Rpc(UserService) export class UserServiceImpl implements UserService { async hello(name: string): Promise { return `Hello, ${name}`; } } // 调用方:注入带类型的远程代理 import { Component } from '@celljs/core'; import { Autorpc } from '@celljs/rpc'; @Component() export class Greeter { @Autorpc(UserService) protected userService: UserService; } ```
可运行的示例应用在 [`examples/`](https://github.com/cellbang/cell/tree/main/examples):`backend-app`、`sample-app`(React)、`security-app`、`jwt-app`、`grpc-app`、`schedule`、`prisma-app`、`docker-app`、`standalone-app`、`mycli`、`tui-app`、`coding-agent-app`、`openapi-app`。 ## 生态 所有包都以 [`@celljs`](https://www.npmjs.com/org/celljs) 作用域发布,同时提供 CommonJS 与 ES Module 两种格式。 | 领域 | 包 | | --- | --- | | 核心([`packages/`](https://github.com/cellbang/cell/tree/main/packages)) | `core`、`logger`、`web`、`mvc`、`http`、`express-adapter`、`serve-static`、`rpc`、`typeorm`、`cache`、`retry`、`schedule`、`cqrs`、`grpc`、`security`、`jwt`、`oauth2-core`、`oauth2-client`、`oauth2-jose`、`validation`、`openapi`、`wechat-pay`、`alipay`、`stripe` | | 云([`cloud-packages/`](https://github.com/cellbang/cell/tree/main/cloud-packages)) | `cloud`、`faas-adapter`、`lambda-adapter`、`fc-adapter`、`scf-adapter`、`vercel-adapter`、`oss`、`cos` | | AI([`ai-packages/`](https://github.com/cellbang/cell/tree/main/ai-packages)) | `ai-core`、`ai-pi`、`ai-agent`、`ai-coding-agent` | | UI([`ui-packages/`](https://github.com/cellbang/cell/tree/main/ui-packages)) | `react`、`widget`、`tui` | | 工具链([`dev-packages/`](https://github.com/cellbang/cell/tree/main/dev-packages)) | `cli`、`vite`、`kit` | | Ta([`apps/ta`](https://github.com/cellbang/cell/tree/main/apps/ta)、[`ta-packages/`](https://github.com/cellbang/cell/tree/main/ta-packages)) | [Ta](https://cell.cellbang.com/zh/ta),基于 Cell 与 pi 的 coding agent(`npm i -g @celljs/ta`):`ta` 命令、`ta serve` 与 Web 客户端、桌面与手机应用 | ## 文档 文档站:**https://cell.cellbang.com/zh/**(中英双语),每个错误码都有对应页面。 - 指南:[快速开始](https://cell.cellbang.com/zh/guide/getting-started)、[项目结构与配置](https://cell.cellbang.com/zh/guide/project)、[组件与依赖注入](https://cell.cellbang.com/zh/guide/components)、[HTTP 接口](https://cell.cellbang.com/zh/guide/web)、[前端与 RPC](https://cell.cellbang.com/zh/guide/frontend)、[测试与检查](https://cell.cellbang.com/zh/guide/testing)、[构建与部署](https://cell.cellbang.com/zh/guide/deployment)、[与 Coding Agent 协作](https://cell.cellbang.com/zh/guide/coding-agents)、[组件目录](https://cell.cellbang.com/zh/guide/packages) - 给 agent 读的:[`llms.txt`](https://cell.cellbang.com/zh/llms.txt)、[`llms-full.txt`](https://cell.cellbang.com/zh/llms-full.txt)(全部文档合成一个文件);项目里用 `npx cell docs <关键词>` 检索与已安装版本一致的文档 - [`cell` 命令行文档](https://cell.cellbang.com/zh/cli):命令、preset、部署器、`cell.config.ts` - [从 Cell 3.x 迁移](https://cell.cellbang.com/zh/migration-4)(Cell 3.x 见标签 `v3.9.0`) - 设计文档:[架构](https://github.com/cellbang/cell/blob/main/docs/design/architecture.md)、[模块](https://github.com/cellbang/cell/blob/main/docs/design/modules.md)、[Agent 优先原则](https://github.com/cellbang/cell/blob/main/docs/design/agent-first.md) - [API 参考](https://cell.cellbang.com/api/)(TypeDoc 生成) - [Ta](https://cell.cellbang.com/zh/ta):基于 Cell 的 coding agent,每个包都有一页 - [更新日志](https://github.com/cellbang/cell/blob/main/CHANGELOG.md) ## 参与贡献 欢迎提交 Issue 和 Pull Request。仓库是 Bun workspace monorepo,使用 Turborepo 与 TypeScript 7 构建。 环境要求:[Bun](https://bun.sh)(版本见 `package.json` 的 `packageManager`)与 Node.js `^20.19.0 || >=22.12.0`。 ```bash bun install # 安装依赖并同步 tsconfig references bun run build # 构建所有包 bun run test # 运行全部测试 bunx turbo run test --filter=@celljs/core # 测试单个包 bun run lint # Biome 检查(bun run format 自动修复) bun changeset # 为下一次发布记录变更 ``` ## 动态 ![Repobeats analytics](https://repobeats.axiom.co/api/embed/59b39c98717cf1ae18b57f24d2efe91617e3a6f1.svg "Repobeats analytics image") ## 许可证 [MIT](https://github.com/cellbang/cell/tree/main/LICENSE) # 快速开始 本页从一个空目录开始,做出一个经过测试、可以构建和部署的 Cell 后端。开发需要 Node.js `^20.19.0 || >=22.12.0`;构建产物可以在 Node.js 18 及以上、Bun 1.2 及以上和 Deno 2 上运行。 ## 创建项目 ```bash npx @celljs/cli init backend my-app # 或 fullstack:React 前端 + 经 RPC 调用的后端 cd my-app npm install npx cell dev # http://127.0.0.1:3000,保存即重载 ``` `cell init` 从不提问。不带模板名时列出全部模板;目录非空时需要 `--force`。生成的项目: ```text my-app/ ├── cell.yml # 应用配置,用 @Value 读取 ├── package.json # 脚本:dev、build、preview、check、test ├── tsconfig.json # experimentalDecorators + emitDecoratorMetadata ├── src/node/ │ ├── module.ts # 后端 DI 模块:导入每个声明了组件的文件 │ └── home-controller.ts ├── test/app.spec.ts # bun test + createTestApp() ├── AGENTS.md、CLAUDE.md # 给 coding agent 的指南,与已安装的版本一致 ├── .agents/skills/cell/ # 同一份指南,以 agent skill 的形式提供 └── .mcp.json # 给支持 MCP 的 agent 启动 `npx cell mcp` ``` ## 添加接口 新建一个控制器,再在 `src/node/module.ts` 里导入它的文件。**没有被任何模块导入的文件不会被加载**,其中的组件也就不存在。 ```ts // src/node/todo-controller.ts import { Body, Controller, Delete, Get, Param, Post } from '@celljs/mvc/lib/node'; import { NotFoundError } from '@celljs/web/lib/node'; interface Todo { id: number; title: string } @Controller('todos') export class TodoController { protected readonly todos = new Map(); protected nextId = 1; @Get() list(): Todo[] { return [...this.todos.values()]; } @Get(':id') get(@Param('id') id: number): Todo { const todo = this.todos.get(id); if (!todo) { throw new NotFoundError(`No todo ${id}`); // 404 } return todo; // 对象以 JSON 返回 } @Post() create(@Body('title') title: string): Todo { const todo = { id: this.nextId++, title }; this.todos.set(todo.id, todo); return todo; } @Delete(':id') remove(@Param('id') id: number): void { this.todos.delete(id); } } ``` ```ts // src/node/module.ts import './home-controller'; import './todo-controller'; import { autoBind } from '@celljs/core'; export default autoBind(); ``` `@celljs/web` 是 `@celljs/mvc` 的依赖;直接从它导入时,也把它加进 `package.json`(`npm install @celljs/web`)。 ```bash curl -X POST localhost:3000/todos -H 'content-type: application/json' -d '{"title":"write docs"}' curl localhost:3000/todos/1 ``` 状态变多时把它移到服务里,见[组件与依赖注入](https://cell.cellbang.com/zh/guide/components)。控制器的装饰器列在 [HTTP 接口](https://cell.cellbang.com/zh/guide/web)。 ## 配置 `cell.yml` 保存应用配置。组件的配置项(`server.port` 等)写在顶层,你自己的配置写在 `app` 下;配置默认只在服务端,只有 `app.public` 和组件标记为 public 的键会进浏览器: ```yaml app: greeting: Hello from my-app todos: max: ${(env.TODOS_MAX ?? 100) * 1} # 服务端启动时求值;app 下的键没有 schema 类型,* 1 把环境变量的文本转成数字 ``` ```ts @Value('app.todos.max') protected readonly max: number; ``` `npx cell config get app.todos.max --why` 给出合并后的值,以及设置过它的每个文件和环境变量。详见[项目结构与配置](https://cell.cellbang.com/zh/guide/project)。 ## 检查与测试 ```bash npx cell check # TypeScript、配置、装饰器设置、DI 绑定 bun test # test/ 下的测试 ``` ```ts // test/todo.spec.ts import { afterAll, beforeAll, describe, expect, it } from 'bun:test'; import { join } from 'node:path'; import { createTestApp, type TestApp } from '@celljs/cli'; describe('todos', () => { let app: TestApp; beforeAll(async () => { app = await createTestApp({ root: join(__dirname, '..') }); }, 60000); afterAll(() => app?.close()); it('creates a todo', async () => { const res = await app.fetch('/todos', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"title":"a"}' }); expect(await res.json()).toEqual({ id: 1, title: 'a' }); }); }); ``` `createTestApp()` 在测试进程里启动整个应用,不监听端口。见[测试与检查](https://cell.cellbang.com/zh/guide/testing)。 ## 构建与运行 ```bash npx cell build # .cell/output/server/index.mjs:内联了全部依赖的单个文件 npx cell preview # 按生产环境的方式运行产物 node .cell/output/server/index.mjs ``` `--single` 把前端也放进这一个文件,`--compile` 生成不需要 Node.js 的可执行文件。 ## 部署 ```bash npx cell deploy --preset cloudflare --dry-run # 只给出计划,不上传 npx cell deploy --preset cloudflare ``` preset 有 `node`(默认)、`cli`、`cloudflare`、`fc`、`scf`、`lambda` 和 `vercel`。见[构建与部署](https://cell.cellbang.com/zh/guide/deployment)。 ## 下一步 - [组件目录](https://cell.cellbang.com/zh/guide/packages):数据库、缓存、定时任务、安全、AI 等需求该加哪个 `@celljs/*` 包。 - [与 Coding Agent 协作](https://cell.cellbang.com/zh/guide/coding-agents):`cell init` 生成的 AGENTS.md、skill 和 MCP 工具。 # 项目结构与配置 ## 目录结构 Cell 项目就是一个 npm 包。源码遵循少量约定,不需要在别处登记。 | 路径 | 内容 | | --- | --- | | `src/node/` | 后端代码:控制器、服务、中间件 | | `src/browser/` | 前端代码(有前端时) | | `src/common/` | 前后端都要导入的代码,例如 RPC 接口 | | `src/node/module.ts` | 后端 DI 模块 | | `src/browser/module.ts` | 前端 DI 模块 | | `cell.yml`、`cell..yml` | 应用配置 | | `cell.config.ts`(可选) | 工具链配置 | | `.cell/` | 生成的文件:`generated/`(DI 装配、合并后的配置)和 `output/`(构建产物);加入 `.gitignore` | ### DI 模块 DI 模块是一个文件:导入声明了组件的文件,并导出 `autoBind()`: ```ts // src/node/module.ts import './todo-controller'; import './todo-service'; import { autoBind } from '@celljs/core'; export default autoBind(); ``` `autoBind()` 登记此前定义的所有带装饰器的类(`@Component`、`@Controller`、`@Rpc`……)。**没有被模块导入的文件永远不会被加载**:如果 `cell check` 对你写的类报 `CELL_DI_NO_BINDING`,在这里导入它的文件。 工具链按以下顺序查找项目的模块文件,存在的都会加载: | 目标 | 文件(在 `src/` 下,扩展名 `.ts`、`.tsx`、`.js` 或 `.mjs`) | | --- | --- | | backend | `common/module`、`node/backend-module`、`node/module`、`module`、`backend-module` | | frontend | `common/module`、`browser/frontend-module`、`browser/module`、`module`、`frontend-module` | `src/common/module` 和 `src/module` 在前后端都会加载。包也可以在 `package.json` 的 `cell.modules` 字段里声明其他路径。 ### 组件 `package.json` 带 `cell` 字段的依赖都是**组件**:安装它,就会把它的 DI 模块和 `cell.yml` 加进应用。装上 `@celljs/security` 就启用登录,装上 `@celljs/serve-static` 就会返回前端文件。要去掉某个依赖带进来的组件,在 `cell.config.ts` 里写: ```ts export default defineConfig({ modules: ['!@celljs/serve-static'] }); ``` `npx cell info` 列出 Cell 解析出的组件、它们的 DI 模块、入口和 preset;`--json` 以数据形式给出同样的内容。 ## 应用配置:`cell.yml` ```yaml # yaml-language-server: $schema=./.cell/schema/cell.yml.json server: port: ${env.PORT ?? 8080} # 组件的配置项直接写在顶层 typeorm: ormConfig: - type: mysql password: ${env.DB_PASSWORD} app: # 你自己的配置写在 app 下 greeting: Hello public: # 只有 app.public 会进入浏览器 apiBase: /api ``` - 顶层键就是配置键,没有 `cell:`、`frontend:`、`backend:` 这类分区。用属性或构造器参数上的 `@Value('server.port')`、`@Value('app.greeting')` 读取。 - **每个顶层键都有归属**:组件在自己的 `cell.schema.json` 里声明它拥有的键(`server` 属于 `@celljs/web`,`security` 属于 `@celljs/security`……)。写在这些键下就是覆盖组件的默认值。 - **你自己的配置放在 `app` 下**,框架和组件都不会占用它,升级也不会撞名。 - 写错键会报错:没有归属的顶层键、组件没有的选项、类型不对,`cell build` / `cell dev` / `cell check` 都会报 `CELL_CONFIG_INVALID`,带文件、行号和"你是不是要写 …"。 - `cell dev` / `cell build` / `cell check` 会生成 `.cell/schema/cell.yml.json`;在 `cell.yml` 第一行加上面的 `yaml-language-server` 注释,编辑器(VS Code 的 YAML 插件等)就有补全和校验。 ### 前端能看到什么 **配置默认只在服务端。** 浏览器只拿到组件标记为 public 的键(如 `server.path`、`rpc`、`react`、`security` 中页面跳转相关的项)和 `app.public`。密钥写在哪里都不会进前端产物,除非你把它放进 `app.public`。 - 前端的值在构建时求值,表达式可以引用服务端的键,只有结果进产物。 - `npx cell config get --target frontend` 查看前端拿到的值;查询服务端独有的键会提示 `server-only`。 - 项目也可以写自己的 `cell.schema.json`,描述 `app` 下的键(类型、说明、`"x-cell-public": true`)。 ### 合并顺序 配置由这些文件按顺序合并:各组件的 `cell.yml`(按依赖顺序)、项目的 `cell.yml`、各个生效 mode 的 `cell..yml`。后面的覆盖前面的;对象深合并,数组整体替换。 ### mode | 命令 | mode | | --- | --- | | `cell dev` | `development` | | `cell build` | `production` | | `createTestApp()` | `test` | 用 `-m `(可重复)加更多:`cell build -m staging` 还会读取 `cell.staging.yml`。没有任何文件的 mode 会被 `cell check` 报出(`CELL_MODE_UNKNOWN`)。 ### 表达式 `${...}` 的求值上下文是配置本身加上 `env`(进程环境变量): ```yaml server: endpoint: http://localhost:${server.port} # 引用其他键 cloud: region: ${env.REGION ?? 'eu-west-1'} # 带默认值的环境变量 typeorm: ormConfig: - port: ${env.DB_PORT ?? 5432} # schema 类型是整数:DB_PORT=6543 得到数字 6543 synchronize: ${env.DB_SYNC ?? false} # schema 类型是布尔:只接受 true / false ``` - 默认值用 `??`,与 JavaScript 相同;**空的环境变量视为未设置**。不需要给值加引号。 - **按 schema 类型转换**:键的类型是数字或布尔时,表达式得到的文本会转换成该类型,转换失败报错并指出键。没有 schema 的键(如 `app` 下的)保留表达式结果的类型。 - **后端的值在服务端启动时求值**,所以同一个产物可以在多个环境运行,密钥也不会写进产物。前端的值在构建时求值。 - 表达式嵌在其他文字里时得到字符串;表达式的结果不会再次求值:环境变量里即使有 `${...}` 也按原文使用。 ### 环境变量 任何已有的键都可以不改文件、用 `CELL_` 开头的环境变量覆盖: ```bash CELL_SERVER__PORT=8080 node .cell/output/server/index.mjs # server.port,启动时生效 CELL_SERVER__HOST=0.0.0.0 # 监听所有网卡(容器里) CELL_APP__PUBLIC__API_BASE=https://api.example.com npx cell build # 前端的 app.public.apiBase,构建时生效 ``` - 前缀后只有一个下划线,层级之间用 `__`(`CELL_SERVER__PORT`)。`CELL_PRESET`、`CELL_MODE`、`CELL_EVENT`、`CELL_OUT_DIR`、`CELL_DEBUG` 是工具链自己的变量,不当作配置;只有一段、又不是顶层键的变量(如别的工具的 `CELL_REGION`)直接忽略;带 `__` 却找不到键的会报告,写成旧的 `CELL__…` / `CELL_BACKEND__…` 时提示正确写法。 - `__` 分隔层级,匹配时不区分大小写,并忽略 `_` 和 `-`(`ORM_CONFIG` 对应 `ormConfig`);数组元素用下标。 - 同一组变量对前后端都生效:服务端启动时应用;`cell build` 应用后再挑出 public 键进前端。 - 只能覆盖已经存在的键;新键先在 `cell.yml` 里写上默认值。 - 值按原值(或 schema)的类型转换:数字、布尔,对象和数组用 JSON。整体是一个 `${...}` 的值会保留并求值:`CELL_SERVER__PORT='${env.PORT}'`。 - 找不到键或类型不符的变量不生效并给出警告;`cell check` 和 `cell info` 会报出它们。 ### 查看值从哪里来 ```bash npx cell config get server.port --why # server.port (backend) = 8080 # server-only, owned by @celljs/web # Port the server listens on. Override with CELL_SERVER__PORT. # node_modules/@celljs/web/cell.yml: 3000 # cell.yml: 8080 ``` 框架的常用配置项: | 键 | 默认值 | 所在包 | | --- | --- | --- | | `server.port` / `server.host` | `3000` / `127.0.0.1` | `@celljs/web` | | `server.path` | `/` | `@celljs/web`(所有路由的前缀,前端可见) | | `web.body.limit` | `100kb` | `@celljs/web` | | `web.cors` | 允许所有来源(`*`) | `@celljs/web` | | `web.session` / `web.cookies` | 见 `@celljs/web` 的 README | `@celljs/web` | | `html.title` / `html.rootId` | `Cell Framework` / `cell-root` | `@celljs/core`(生成的 HTML 页面) | | `logger.level` | `info` | `@celljs/core` | | `rpc.path` | `api` | `@celljs/rpc` | | `serveStatic.spa` | `true` | `@celljs/serve-static` | 各包的 README 列出了自己的配置项;完整的键、类型和说明在 `.cell/schema/cell.yml.json`。设计说明见 [docs/design/config.md](https://github.com/cellbang/cell/blob/main/docs/design/config.md)。 ## 工具链配置:`cell.config.ts` 可选,只用于工具链。应用配置不写在这里。 ```ts import { defineConfig } from '@celljs/cli'; export default defineConfig({ preset: 'cloudflare', // 默认 node modules: ['!@celljs/serve-static'], // 去掉一个组件 build: { external: ['sharp'], sourcemap: true }, vite: { resolve: { alias: { '~': '/src' } } }, // 合并进 Vite 配置 hooks: { 'build:before': 'prisma generate' }, // 在工具链事件上执行的 shell 命令 fc: { region: 'cn-shanghai', memory: 512 }, // preset 的选项,写在与 preset 同名的字段 }); ``` 未知字段会报错并给出最接近的合法名字(`CELL_CONFIG_INVALID`、`CELL_CONFIG_UNKNOWN_OPTION`)。完整说明(包括添加 Vite 插件、检查项、MCP 工具或产物文件的 toolchain 模块)见 [CLI 文档](https://cell.cellbang.com/zh/cli)。 ## 生成的文件 `cell dev` 和 `cell build` 把生成的代码写到 `.cell/generated/`:每个目标加载的 DI 模块、合并后的配置和项目模型。需要确切知道容器里装了什么时就读这些文件;不要修改它们。 # 组件与依赖注入 Cell 应用的每个部分都是**组件**:由 DI 容器创建、装配和持有的类。控制器、服务、中间件和 RPC 服务都是组件。容器是 Cell 自己实现的(`@celljs/core`),不依赖第三方 IoC 库。 ## 声明与注入 ```ts import { Autowired, Component, Value } from '@celljs/core'; @Component() export class TodoRepository { protected readonly todos: string[] = []; add(title: string) { this.todos.push(title); } all() { return [...this.todos]; } } @Component() export class TodoService { @Autowired() // 按声明的类型注入 protected readonly repository: TodoRepository; @Value('app.todos.max') // 配置,见 cell.yml protected readonly max: number; add(title: string) { if (this.repository.all().length >= this.max) { throw new Error('Too many todos'); } this.repository.add(title); } } ``` - 组件默认是**单例**:每个应用一个实例。`@Component({ scope: Scope.Transient })` 每次注入都创建新实例。 - 可以注入到属性(如上)或构造器参数(`constructor(@Autowired(TodoRepository) protected readonly repository: TodoRepository)`)。没有装饰器的构造器参数按声明的类注入。 - 文件必须被某个 [DI 模块](https://cell.cellbang.com/zh/guide/project#di-模块)导入,否则类不会被登记。 - `@Value('key')` 读取合并后的 `cell.yml`;不带键的 `@Value()` 以属性名为键。 ## 接口:同名的 Symbol TypeScript 的接口在运行时不存在,所以接口的标识是一个同名导出的 `Symbol`。这是整个 Cell 的约定: ```ts // src/common/mailer.ts export const Mailer = Symbol('Mailer'); export interface Mailer { send(to: string, text: string): Promise; } ``` ```ts // src/node/mailer-impl.ts import { Component } from '@celljs/core'; import { Mailer } from '../common/mailer'; @Component(Mailer) export class MailerImpl implements Mailer { async send(to: string, text: string): Promise { /* ... */ } } // 使用处 @Autowired(Mailer) protected readonly mailer: Mailer; ``` `@Component(Mailer)` 把类同时登记在这个 symbol **和**类本身之下,两者取到的是同一个实例。`@Component({ id: [A, B] })` 登记更多标识。 ## 多个实现 | 需求 | 写法 | | --- | --- | | 拿到全部实现 | `@Autowired(Handler) protected readonly handlers: Handler[]`(数组类型) | | 允许没有 | 加 `@Optional()`:单值为 `undefined`,数组为 `[]` | | 按名字选 | `@Component({ id: Store, name: 'redis' })` 与 `@Autowired(Store) @Named('redis')` | | 按标签选 | `@Component({ id: Store, tag: { tag: 'region', value: 'eu' } })` 与 `@Autowired(Store) @Tagged('region', 'eu')` | | 多个里有一个默认 | `@Component({ id: Store, default: true })`:普通的 `@Autowired(Store)` 拿到它 | | 替换框架的组件 | `@Component({ id: ErrorHandler, rebind: true })`:移除该标识之前的绑定 | | 多个、排序、延迟创建 | `@AutowiredProvider(Step) protected readonly steps: Provider`,再 `this.steps.sortSync()`(`priority` 大的在前) | 框架的扩展点(`Middleware`、`ErrorHandler`、`ApplicationLifecycle`、`HealthIndicator`……)都是这样的多实现 symbol:在类上声明 `@Component(Middleware)` 就是新增一个实现,`rebind: true` 则替换内置实现。 ## 生命周期 ```ts import { Application, ApplicationLifecycle, Component, PostConstruct } from '@celljs/core'; @Component(ApplicationLifecycle) export class Warmup implements ApplicationLifecycle { initialize(): void { /* 同步执行,在所有 onStart 之前 */ } async onStart(app: Application): Promise { /* 逐个 await */ } onStop(app: Application): void { /* 停止时,以及 cell dev 每次重载之前 */ } } @Component() export class Cache { @PostConstruct() protected init(): void { /* 属性注入完成后,每个实例一次 */ } } ``` `initialize`、`onStart`、`onStop` 抛出的错误只记日志,不影响其他生命周期。不能在模块加载时做的事(打开连接、定时器)放在 `onStart`,不要放在构造器:`cell check` 会创建组件,但不启动应用。 ## 日志 ```ts import { Autowired, Component, Logger } from '@celljs/core'; @Component() export class TodoService { @Autowired(Logger) protected readonly logger: Logger; add(title: string) { this.logger.info(`added ${title}`); } } ``` 级别来自 `logger.level`(`verbose`、`debug`、`info`(默认)、`warn`、`error`)。`@celljs/logger` 把控制台日志换成 winston。 ## 切面(AOP) 切面在 `proxy: true` 的组件的方法前后执行代码(控制器和 RPC 服务默认被代理): ```ts import { Aspect, MethodBeforeAdvice } from '@celljs/core'; @Aspect({ id: MethodBeforeAdvice, pointcut: 'Audited' }) export class AuditAdvice implements MethodBeforeAdvice { async before(method: string | number | symbol, args: any[], target: any): Promise { console.log('call', String(method), args); } } @Component({ proxy: true, sysTags: ['Audited'] }) export class AccountService { /* ... */ } ``` `AfterReturningAdvice` 和 `AfterThrowsAdvice` 在方法返回或抛错后执行。不写 `pointcut` 的通知作用于所有被代理的组件。被代理的方法总是返回 Promise,同步方法也一样。`aop.enabled: false` 关闭代理。 ## 容器之外 没有全局容器。运行时才决定取哪个组件的组件注入容器本身(`@Autowired(Container)`);不是组件的代码(脚本、测试)用自己创建的容器:`ApplicationFactory.createContainer(props, ...modules)`(`@celljs/core/lib/common/application/application-factory`)返回它,[`createTestApp()`](https://cell.cellbang.com/zh/guide/testing) 有 `app.get()`。`{ detached: true }` 的属性从创建实例的容器里取值,所以只对组件有效。 ## 装配失败时 容器把装配问题报成结构化错误,`npx cell check` 不启动应用就能发现它们: | 错误码 | 含义 | 通常的修法 | | --- | --- | --- | | `CELL_DI_NO_BINDING` | 某个标识没有任何绑定 | 在 DI 模块里导入该文件;加 `@Component(Id)`;安装对应的包 | | `CELL_DI_AMBIGUOUS` | 单值注入匹配到多个绑定 | 改为注入数组,或用 `@Named` / `@Tagged` / `default: true`,或 `rebind: true` | | `CELL_DI_CIRCULAR` | 组件在创建过程中互相依赖 | 让一方延迟获取(`@AutowiredProvider`,或属性上的 `@Autowired({ detached: true })`),或把共用部分移到另一个组件 | | `CELL_DI_MISSING_INJECT` | 构造器参数没有标识 | 加 `@Autowired(Id)` | 错误信息里有缺少的标识、谁需要它以及解析链。每个装饰器的详细说明见 [core 文档](https://cell.cellbang.com/zh/packages/core)。 # HTTP 接口 Cell 的 HTTP 由两个包组成:`@celljs/web`(服务器、中间件链、请求体解析、cookie、session、CORS、错误处理)和建在它之上的 `@celljs/mvc`(控制器)。两者都运行在 Node 的 `node:http` 上,不依赖第三方框架。装饰器从 `@celljs/mvc/lib/node` 导入,请求相关的类型从 `@celljs/web/lib/node` 导入。 ## 控制器与路由 ```ts import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@celljs/mvc/lib/node'; @Controller('users') // 前缀:/users export class UserController { @Get() // GET /users?role=admin list(@Query('role') role?? string) { /* ... */ } @Get(':id') // GET /users/42 get(@Param('id') id: number) { /* ... */ } @Post() create(@Body() user: CreateUser) { /* ... */ } @Patch(':id') rename(@Param('id') id: number, @Body('name') name: string) { /* ... */ } @Delete(':id') remove(@Param('id') id: number): void { /* ... */ } } ``` - 方法装饰器:`@Get`、`@Post`、`@Put`、`@Patch`、`@Delete`、`@Head`、`@Options`、`@All`(所有方法)。不带路径时响应控制器路径本身。 - 路径:`:name` 匹配一段,`*` 匹配任意内容,`(...)` 表示可选。`server.path`(默认 `/`)是所有路由的前缀。 - 没有路由匹配的请求得到 `404`。 - 控制器也是组件:用 `@Autowired` 注入服务,用 `@Value` 读配置。记得在 `src/node/module.ts` 里导入控制器的文件。 ## 参数 | 装饰器 | 取到的值 | | --- | --- | | `@Param('id')` / `@Param()` | 一个路径参数 / 全部 | | `@Query('q')` / `@Query()` | 一个查询参数 / 解析后的整个查询(`?a[b]=1` → `{ a: { b: '1' } }`) | | `@Body('name')` / `@Body()` | 请求体的一个字段 / 整个请求体 | | `@Header('x-api-key')` / `@Header()` | 一个请求头 / 全部请求头 | | `@Cookie('sid')` | 一个请求 cookie | | `@Session('cart')` | session 里的一个值 | | `@Req()` / `@Res()` | Node 的请求和响应对象(`IncomingMessage` / `ServerResponse`,带扩展) | 类型为 `string`、`number`、`boolean` 或 `Date` 的参数会被转换(`'42'` → `42`)。请求体按 JSON、`text/plain`、`application/octet-stream`(Buffer)或 URL 编码表单解析,gzip、deflate、br 压缩的请求体会被解压。非法 JSON 返回 `400`,超过 `web.body.limit`(默认 `100kb`)返回 `413`。 ### 校验请求体 加上 `@celljs/validation` 和 `class-validator`,再用类来声明参数类型: ```ts import { IsString, Length } from 'class-validator'; export class CreateUser { @IsString() @Length(1, 20) name: string; } @Post() create(@Body() user: CreateUser) { /* user 是校验通过的 CreateUser 实例 */ } ``` 请求体不合法时返回 `400` 和 JSON 格式的错误,方法不会被调用。选项在 `validation` 下([@celljs/validation](https://cell.cellbang.com/zh/packages/validation))。 ## 响应 | 返回值 / 装饰器 | 响应 | | --- | --- | | 对象、数组、数字…… | JSON(`application/json`),默认视图 | | `@Text()` 并返回字符串 | `text/plain` | | `@Html('page.mustache')` 并返回对象 | 用它渲染 `mvc.mustache.baseViewDir`(默认 `assets/views`)里的 Mustache 模板 | | `@File()` 并返回 Buffer 或流 | 原样以流输出 | | `undefined`(`void`) | `200`,空响应体 | | `@Header('cache-control', 'no-store')` | 添加响应头 | | `@Cookie('flavor', 'oat')` / `@Session('step', 'two')` | 设置 cookie / session 值 | `async` 方法同样适用。要指定状态码,取得响应对象:`@Res() res: Response`(`Response` 是接口,用 `import type { Response } from '@celljs/web/lib/node'` 导入)或 `Context.getResponse()`,设置 `res.statusCode = 201`,再照常返回响应体。 ## 错误 ```ts import { HttpError, NotFoundError } from '@celljs/web/lib/node'; throw new NotFoundError('No user 42'); // 404 throw new HttpError(409, 'Name already taken'); // 任意状态码 throw new Error('boom'); // 500 ``` 状态码取自 `HttpError.statusCode`;其他错误一律返回 `500`。调用栈从不发给客户端:`500` 连同调用栈(以及 `cause`)记在 error 级别的日志里,4xx 的 `HttpError`(以及被处理器以重定向或 4xx 回答的错误,如需要登录、无权访问)只记在 debug 级别。客户端能看到 4xx `HttpError` 的信息;生产环境下 5xx 和其他错误只返回状态文本,除非 `HttpError` 创建时带了 `{ expose: true }`,或设置了 `web.errors.exposeMessages`。 **单个控制器内**,带 `@Catch(ErrorType)` 的方法处理本控制器抛出的该类型错误,它的返回值就是响应: ```ts @Catch(QuotaError) @Text() onQuota(err: QuotaError) { return `Quota exceeded: ${err.message}`; } ``` **整个应用**,添加一个 `ErrorHandler`。按 `priority` 从高到低询问各个处理器能否处理(`canHandle`),由第一个能处理的处理;它的 `handle` 抛错时再试下一个。内置处理器的优先级最高到 `2000`,你的要更高才会先执行: ```ts import { Component } from '@celljs/core'; import { Context, ErrorHandler } from '@celljs/web/lib/node'; @Component(ErrorHandler) export class QuotaErrorHandler implements ErrorHandler { readonly priority = 2100; async canHandle(ctx: Context, err: Error) { return err instanceof QuotaError; } async handle(ctx: Context, err: Error) { ctx.response.statusCode = 429; ctx.response.setHeader('content-type', 'application/json'); ctx.response.end(JSON.stringify({ error: err.message })); } } ``` ## 中间件 中间件是绑定到 `Middleware` 的组件,在控制器之前看到每个请求;`priority` 越大越早执行。 ```ts import { Component } from '@celljs/core'; import { Context, HTTP_MIDDLEWARE_PRIORITY, HttpError, Middleware } from '@celljs/web/lib/node'; @Component(Middleware) export class ApiKeyMiddleware implements Middleware { readonly priority = HTTP_MIDDLEWARE_PRIORITY - 10; // 在 trace、CORS 和请求体解析之后 async handle(ctx: Context, next: () => Promise): Promise { if (ctx.request.path.startsWith('/admin') && ctx.request.get('x-api-key') !== process.env.API_KEY) { throw new HttpError(401, 'Missing or wrong API key'); } await next(); } } ``` 只需要 Node 的 `req` / `res` 的 express 中间件可以用 `convertExpressMiddleware(fn)` 包装后使用。 ### 请求上下文 请求处理期间的任何位置(包括离控制器很远的服务),都能用 `@celljs/web/lib/node` 的 `Context` 拿到当前请求:`Context.getRequest()`、`Context.getResponse()`、`Context.getSession()`、`Context.getCookies()`、`Context.getTraceId()`,以及请求作用域的属性 `Context.setAttr(key, value)` / `Context.getAttr(key)`。它跨 `await`、定时器和 Promise 链保持有效。 ## 内置功能 | 功能 | 配置 | | --- | --- | | Trace ID:每个响应都带 `X-Cell-Trace-ID` 头(`Context.getTraceId()`) | `web.trace` | | 健康检查:`GET /health` | `web.health` | | CORS 响应头,默认允许所有来源 | `web.cors`(`origin`、`methods`、`credentials`……) | | 签名 cookie,以及存放在 cookie 里的 session | `web.cookies.keys`、`web.session`。**生产环境务必设置自己的 `web.cookies.keys`** | | 请求体限制 | `web.body` | 全部选项见 [@celljs/web 文档](https://cell.cellbang.com/zh/packages/web)。 ## 更多 | 需求 | 包 | | --- | --- | | 从控制器生成 OpenAPI 文档(`/openapi.json`)和 Swagger UI(`/api-docs`) | [`@celljs/openapi`](https://cell.cellbang.com/zh/packages/openapi) | | 登录、session、`@Authenticated()`、`@PreAuthorize()` | [`@celljs/security`](https://cell.cellbang.com/zh/packages/security) | | JSON Web Token | [`@celljs/jwt`](https://cell.cellbang.com/zh/packages/jwt) | | 返回前端构建产物 | [`@celljs/serve-static`](https://cell.cellbang.com/zh/packages/serve-static) | | 从前端或其他服务做类型安全的调用 | [前端与 RPC](https://cell.cellbang.com/zh/guide/frontend) | | gRPC 服务 | [`@celljs/grpc`](https://cell.cellbang.com/zh/packages/grpc) | # 前端与 RPC Cell 应用可以在同一个项目里同时有 React 前端和后端。`cell dev` 在一个端口上同时提供两者(前端 HMR、后端热重载),`cell build` 把前端放到 `.cell/output/public/`,由后端经 `@celljs/serve-static` 返回,或由平台的 CDN 托管(Cloudflare、Vercel)。 ```bash npx @celljs/cli init fullstack my-app ``` fullstack 模板依赖 `@celljs/react`(它会加上 `frontend` 构建目标)、`@celljs/rpc` 和 `@celljs/serve-static`。 ## 页面 页面就是 `src/browser/pages/` 下的文件,每个文件的默认导出是页面组件。 ```text src/browser/pages/ ├── _layout.tsx 包在它下面所有页面外层 ├── index.tsx / ├── about.tsx /about └── users/ ├── index.tsx /users └── [id].tsx /users/:id ``` ```tsx // src/browser/pages/users/[id].tsx import { Link, useParams } from '@celljs/react'; export default function User() { const { id } = useParams(); return <>

User {id}

All users; } ``` ```tsx // src/browser/pages/_layout.tsx import { NavLink } from '@celljs/react'; import type { PropsWithChildren } from 'react'; export default function Layout({ children }: PropsWithChildren) { return <>{children}; } ``` - `[...rest].tsx` 匹配剩余的整段路径:`pages/[...rest].tsx` 就是未知 URL 的页面。 - `_layout.tsx` 通过 `children` 拿到页面。布局逐层嵌套(外层在前),在它下面的页面之间切换时保持挂载;没有 `_layout` 的页面使用 `@DefaultLayout`。 - 页面渲染时抛错、或者它的代码块加载失败,只在它的布局里显示错误,不会让整个应用白屏;用户去别的路径时会重新尝试。显示的是最近的 `_error.tsx`(同目录或上级),props 是 `{ error, reset }`(`@celljs/react` 的 `ErrorFallbackProps`);没有时显示错误信息和一个重试按钮。 - 以 `_` 开头的文件和目录、以及测试文件不是页面:页面用到的组件可以放在旁边的 `_components/` 里。 - 每个页面单独成一个 chunk,访问时才加载。两个文件对应同一路径(`about.tsx` 与 `about/index.tsx`)会报 `CELL_ROUTE_CONFLICT`。 - 不需要在任何地方加 import:`cell dev` 会感知页面的增删。路由生成在 `.cell/generated/frontend/routes.js`,`npx cell info --json` 会列出它们(`frontend.routes`)。 路由基于 React Router 7。`@celljs/react` 重新导出了页面常用的部分:`Link`、`NavLink`、`Navigate`、`Outlet`、`useParams`、`useNavigate`、`useLocation`、`useSearchParams`、`useMatch`。`cell.yml` 里的 `react.router.type` 选择 `browser`(默认)或 `hash` 路由。`@celljs/react` 的 `@App`、`@DefaultLayout` 和 `@Router` 分别替换根组件、默认布局和路由器;包可以用 `@View` 添加页面(见参考文档)。 `src/browser/module.ts` 是前端的 DI 模块:它导入前端服务所在的文件(见下文)。 前端拿到的是组件标记为 public 的配置键和你的 `app.public`,在构建时求值,对所有人可见。在浏览器里,React 组件用 `useValue('html.title')` 读取,DI 组件用 `@Value`。 ## 在组件里使用服务 前端服务(状态、接口客户端、共享的逻辑)和后端一样是 DI 组件。React 组件用 `@celljs/react` 的 hook 获取它们和配置: ```tsx import { useInject, useValue } from '@celljs/react'; import { UserService } from './user-service'; // 在 module.ts 导入的文件里用 @Component(UserService) 声明 function Header() { const userService = useInject(UserService); // 每个组件实例解析一次 const title = useValue('app.public.title', 'My app'); return
{title}: {userService.name}
; } ``` shell 通过 `ContainerContext` 提供应用的容器。测试组件时,把它渲染在 `` 里,传入你自己的容器。页面、布局和 context 保持为普通的 React 组件,它们用到的东西从容器里取。 `useRpc(id)` 取后端服务(见下一节),`useLocale()` 返回 `[locale, setLocale]`:用户在 `widget.locales` 里选的语言,记在 `localStorage`。要翻译文案,安装 react-intl,并在 `src/browser/module.ts` 里导入一次 `@celljs/react/lib/browser/intl`,之后 `useIntl()` 和 `` 跟随所选语言。全部 hook 见 [@celljs/react 的 README](https://cell.cellbang.com/zh/packages/react)。 ## 调用后端:RPC 不必为自己的前端写 HTTP 接口和 fetch 调用:共享一个接口,像调用本地对象一样调用它。 ```ts // src/common/greeting-protocol.ts:前后端都会导入 export const GreetingServer = Symbol('GreetingServer'); export interface GreetingServer { greet(name: string): Promise; } ``` ```ts // src/node/greeting-server.ts:由 src/node/module.ts 导入 import { Rpc } from '@celljs/rpc'; import { GreetingServer } from '../common/greeting-protocol'; @Rpc(GreetingServer) export class GreetingServerImpl implements GreetingServer { async greet(name: string): Promise { return `Hello, ${name}!`; } } ``` ```tsx // src/browser/pages/index.tsx import { useRpc } from '@celljs/react'; import { GreetingServer } from '../../common/greeting-protocol'; export default function Home() { const greetingServer = useRpc(GreetingServer); const [message, setMessage] = React.useState('Loading…'); React.useEffect(() => { greetingServer.greet('browser').then(setMessage); }, [greetingServer]); return

{message}

; } ``` - `@Rpc(Id)` 既是组件(照常往里注入服务),也是一个端点:以 JSON-RPC over HTTP `POST` 提供在 `//`,默认为 `/api/GreetingServer`(symbol 的描述)。 - 每个方法都返回 Promise;参数和结果以 JSON 传输(JSON-RPC 2.0)。只能调用类自己声明的方法。服务端抛出的错误会让调用方的 Promise reject:`ApplicationError` 原样还原,其他错误是带 `status` 的 `RpcError`;生产环境下除 4xx 的 `HttpError` 外不暴露错误信息。 - 安装了 `@celljs/validation` 时,类类型的参数会像控制器一样被校验。 - 测试里 [`createTestApp()`](https://cell.cellbang.com/zh/guide/testing) 的 `app.get(GreetingServer)` 直接调用实现。 ### 后端调用前端:WebSocket ```yaml rpc: transport: websocket ``` 此后每个浏览器标签页的调用走一条 WebSocket 连接(`//ws`),与 HTTP 一样经过中间件、会话和安全校验,后端也能反过来调用前端。前端提供的能力用 `@RpcClient` 声明(接口放在 `src/common/`,实现类放在 `src/browser/` 并由前端模块导入): ```ts // src/browser/notice-client.ts import { RpcClient } from '@celljs/rpc'; import { NoticeClient } from '../common/notice-protocol'; @RpcClient(NoticeClient) export class NoticeClientImpl implements NoticeClient { async notify(text: string): Promise { console.log(text); } } ``` ```ts // 后端某个 @Rpc 服务里 import { RpcContext } from '@celljs/rpc/lib/node'; const connection = RpcContext.requireConnection(); // 调用方所在的标签页 await connection.client(NoticeClient).notify('order shipped'); connection.onClose(() => { /* 标签页已关闭 */ }); ``` `RpcConnectionManager`(`@celljs/rpc/lib/node`)列出所有打开的连接,可以用来通知全部标签页。无法建立连接的地方(Serverless 预设、不支持 WebSocket 的代理)调用改走 HTTP,此时 `RpcContext.requireConnection()` 会抛 `RpcNoConnectionError`。其他源的页面不能连接,除非列在 `rpc.websocket.allowedOrigins` 里。其余配置见[参考文档](https://cell.cellbang.com/zh/packages/rpc)。 ### 从另一个后端调用 后端用注入的代理调用另一个 Cell 后端的服务: ```ts import { Component } from '@celljs/core'; import { Autorpc } from '@celljs/rpc'; @Component() export class Greeter { @Autorpc(GreetingServer) protected readonly greetingServer: GreetingServer; } ``` ```yaml rpc: defaultEndpoint: "http://greetings.internal:3000{rpcPath}/{serviceIdentifier}" endpoint: # 按服务指定,优先于 defaultEndpoint GreetingServer: "${env.GREETING_URL}" ``` `RpcUtil.create(endpoint, Id)` 不经容器创建同样的代理。 ## 只有前端或只有后端的项目 构建目标随已安装的组件而定:`@celljs/react` 加上 `frontend`,`backend` 总是存在。`cell.config.ts` 的 `targets` 可以显式指定。 注解和配置项的完整列表见 [@celljs/react 文档](https://cell.cellbang.com/zh/packages/react)。 # 测试与检查 每次改动之后,有两个工具帮你闭环:`cell check` 不运行应用就能发现错误,`createTestApp()` 在测试进程里运行应用。 ## `cell check` ```bash npx cell check # 有错误时退出码为 1 npx cell check --json # 一个 JSON 文档:passed、summary、issues ``` | 检查 | 发现的问题 | | --- | --- | | `config` | 缺少入口、未知的 `-m` mode、匹配不到任何键的 `CELL_*` 环境变量 | | `types` | 项目的 `tsc --noEmit`,每条诊断带文件、行号和 `TSxxxx` 码 | | `decorators` | `tsconfig.json` 缺少 `experimentalDecorators` / `emitDecoratorMetadata` | | `di` | 不启动应用、加载后端容器并逐个解析绑定:`CELL_DI_NO_BINDING`、`CELL_DI_AMBIGUOUS`、`CELL_DI_CIRCULAR`…… | 每个问题都有 `code`、已知时的 `file` 和 `line`,以及 `hint`:一句话的修复建议。`--skip ` 跳过某项;组件可以注册自己的检查(例如 `typeorm:entities`)。 ## `createTestApp()` ```ts // test/users.spec.ts import { afterAll, beforeAll, describe, expect, it } from 'bun:test'; import { join } from 'node:path'; import { createTestApp, type TestApp } from '@celljs/cli'; describe('users', () => { let app: TestApp; beforeAll(async () => { app = await createTestApp({ root: join(__dirname, '..') }); }, 60000); // 第一次启动要编译源码 afterAll(() => app?.close()); // 停止应用:触发 onStop it('creates a user over HTTP', async () => { const res = await app.fetch('/users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'Ada' }), }); expect(res.status).toBe(200); expect(await res.json()).toMatchObject({ name: 'Ada' }); }); it('calls a service directly', async () => { const { UserService } = await app.import('/src/node/user-service.ts'); // 应用加载的那份模块 expect(await app.get(UserService).count()).toBe(1); }); }); ``` - 后端按 `cell dev` 的方式从源码加载:不构建,也不监听端口。`fetch(path, init)` 让请求走完整个 HTTP 处理链(中间件、安全、控制器、错误处理器),返回标准的 `Response`。 - `app.get(Id)` 返回应用里的组件,`app.container.getAll(Id)` 返回全部。应用运行在自己的模块实例里:**类要从 `await app.import('/src/node/user-service.ts')` 取**(包写包名),不要在测试文件里直接 `import`。测试自己导入的类或 `Symbol` 按名字匹配,对 symbol 和没有注入的类有效,但带 `@Value` / `@Autowired` 的类导入到测试进程后会报 `CELL_DI_NO_BINDING`。 - 生效的 mode 是 `test`:测试专用的配置写在 `cell.test.yml`,例如内存数据库。`createTestApp({ mode: ['seed'] })` 在 `test` 之后追加 mode。 - 启动失败时抛出 `CellError`(`CELL_DEV_BACKEND_FAILED`,带出错的文件)。没有 HTTP 后端时 `fetch` 抛出 `CELL_TEST_APP_NO_HTTP`。 用 `bun test` 运行测试(模板的 `npm test` 是 `bun test ./test`)。每个测试文件一个应用;同一文件里的测试共享状态,所以要么让它们与顺序无关,要么在 `beforeEach` 里重置数据。 ### 示例:测试数据库 ```yaml # cell.test.yml typeorm: ormConfig: - type: sqljs # 内存数据库,来自 sql.js 包 synchronize: true ``` ## 不启动应用的单元测试 普通的类可以直接 `new`,手动传入协作对象。用属性注入的组件需要容器:优先用 `createTestApp()`,而不是自己组装容器。 ## 调试运行中的应用 `cell dev` 运行时,同一端口上的 `POST /_cell/mcp` 是给 coding agent 用的 MCP 服务器,提供 `get_errors`(启动错误、失败的重载、5xx 请求、错误日志)、`list_routes`、`list_bindings`、`get_config`、`recent_requests` 和 `logs`。见[与 Coding Agent 协作](https://cell.cellbang.com/zh/guide/coding-agents)。 # 构建与部署 在不同平台之间切换不需要改代码:构建时选择的 **preset** 决定产物形态和部署方式。 ```bash npx cell build # preset node:.cell/output npx cell build --preset cloudflare npx cell deploy --preset fc --dry-run # 只给出计划,不上传 ``` preset 的来源依次是:`--preset`,`cell.config.ts` 的 `preset`,`CELL_PRESET` 环境变量,CI 平台识别(Workers Builds 中为 `cloudflare`,Vercel 上为 `vercel`),最后是 `node`。`npx cell info` 会显示用的是哪个以及原因。 ## preset | preset | 产物 | 本地运行 | 部署 | | --- | --- | --- | --- | | `node`(默认) | Node.js HTTP 服务,`server/index.mjs` | `cell preview` | 把 `.cell/output` 拷到任何能运行 Node.js 18+、Bun 或 Deno 的地方 | | `cli` | 基于 Cell DI 的命令行程序,不起 HTTP 服务 | `cell preview` | 分发文件或可执行文件 | | `cloudflare` | Worker + 静态资源 + `wrangler.json` | `cell preview`(经 Wrangler 在 workerd 中运行) | `cell deploy --preset cloudflare` | | `fc` | 阿里云函数计算 3.0 自定义运行时 | `cell preview` | `cell deploy --preset fc` | | `scf` | 腾讯云 SCF Web 函数 | `cell preview` | `cell deploy --preset scf` | | `lambda` | AWS Lambda + 函数 URL | `cell preview` | `cell deploy --preset lambda` | | `vercel` | Vercel Build Output API(`.vercel/output`) | `cell preview` | `cell deploy --preset vercel` | ## 产物 ```text .cell/output/ ├── server/index.mjs # 后端:内联了全部依赖的单个 ES 模块 ├── public/ # 前端产物(有前端时) └── manifest.json # preset、格式、目标:cell preview 和 cell deploy 读取它 ``` 产物不需要项目的 `node_modules`。无法打包的包(原生模块、`cell.config.ts` 的 `build.external`)会拷到 `server/node_modules`。 | 选项 | 结果 | | --- | --- | | `--single` | 前端内嵌进 `server/index.mjs`:一个文件运行整个应用 | | `--compile [bun\|node\|deno]` | 可执行文件 `.cell/output/app`,主机上不需要 Node.js | | `-m ` | 额外读取 `cell..yml`(构建时 `production` 总是生效) | | `--sourcemap`、`--format cjs`、`--out-dir ` | source map、CommonJS 产物、其他输出目录 | ## 生产环境的配置 - **后端配置在服务端启动时求值。** 同一份产物到处运行;各环境的值通过环境变量给出:在 `cell.yml` 里引用(`"${env.DATABASE_URL}"`),或直接覆盖(`CELL_SERVER__PORT=8080`)。见[项目结构与配置](https://cell.cellbang.com/zh/guide/project)。 - **不要把密钥标成 public。** 配置默认只在服务端,只有 schema 标记为 public 的键和 `app.public` 会进前端;前端的值在构建时求值并发给浏览器。 - 服务默认监听 `127.0.0.1:3000`。容器里设置 `CELL_SERVER__HOST=0.0.0.0`;平台通过 `PORT` 给端口时设置 `CELL_SERVER__PORT='${env.PORT}'`。 - 设置自己的 `web.cookies.keys`(cookie 和 session 的签名密钥),例如 `CELL_WEB__COOKIES__KEYS='["..."]'`。 - **安全检查。** `cell check` 会列出在生产环境不安全的设置(`CELL_SECURITY_*` 警告:开发用的 cookie 密钥、过短的 `jwt.secret` 等)。生产启动时(`NODE_ENV=production`,`cell build` 会把它写进服务端产物)同样会把这些警告写进日志;设置 `securityChecks: fail`(或 `CELL_SECURITY_CHECKS=fail`)则拒绝启动,`off` 则不检查。 - **错误信息。** 生产环境下服务端错误只返回状态文本(`Internal Server Error`),错误信息和调用栈写进日志;4xx 的 `HttpError` 信息照常返回。可用 `web.errors.exposeMessages` 改变这一行为。 ### 容器镜像 ```dockerfile FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npx cell build FROM node:22-alpine ENV NODE_ENV=production ENV CELL_SERVER__HOST=0.0.0.0 WORKDIR /app COPY --from=build --chown=node:node /app/.cell/output ./ USER node EXPOSE 3000 CMD ["node", "server/index.mjs"] ``` ## `cell deploy` `cell deploy` 用 preset 构建,生成计划(要创建或更新的资源),再执行它。`--dry-run` 到计划为止,没有凭据也能用;`--skip-build` 部署已有的产物。 | preset | 凭据(环境变量) | `cell.config.ts` 中的平台选项 | | --- | --- | --- | | `cloudflare` | `CLOUDFLARE_API_TOKEN`(及 `CLOUDFLARE_ACCOUNT_ID`),或 `npx wrangler login` | 项目根目录的 `wrangler.jsonc`:名称、vars、绑定、routes | | `fc` | `ALIBABA_CLOUD_ACCESS_KEY_ID`、`ALIBABA_CLOUD_ACCESS_KEY_SECRET` | `fc: { name, region, memory, cpu, timeout, concurrency, env }` | | `scf` | `TENCENTCLOUD_SECRET_ID`、`TENCENTCLOUD_SECRET_KEY` | `scf: { name, region, namespace, memory, timeout, env }` | | `lambda` | `AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY`,或 `AWS_PROFILE` | `lambda: { name, region, architecture, memory, timeout, role, env }` | | `vercel` | `VERCEL_TOKEN`,或 `npx vercel login` | `vercel: { project, scope, environment, memory, maxDuration, regions }` | ```ts // cell.config.ts import { defineConfig } from '@celljs/cli'; export default defineConfig({ preset: 'fc', fc: { region: 'cn-shanghai', memory: 512, env: { DATABASE_URL: process.env.DATABASE_URL ?? '' } }, }); ``` 函数平台(`fc`、`scf`、`lambda`)直接调用云 API,不安装云厂商的 SDK 或 CLI。Wrangler 和 Vercel CLI 优先用项目里安装的,否则经 `npx` 使用。每种失败都有错误码:`CELL_DEPLOY_CREDENTIALS_MISSING`、`CELL_DEPLOY_UNAUTHORIZED`、`CELL_DEPLOY_PACKAGE_TOO_LARGE`…… 各平台的细节(自定义域名、运行时、代码包大小上限、部署器会创建什么)见 [CLI 文档](https://cell.cellbang.com/zh/cli#preset)。 ## hooks 在工具链事件上执行 shell 命令,用于代码生成之类的步骤: ```ts export default defineConfig({ hooks: { 'build:before': 'prisma generate', // 失败会取消构建 'output:done': 'cp extra/* "$CELL_OUT_DIR/server/"', }, }); ``` 需要写代码的扩展(Vite 插件、额外的产物文件、检查项、MCP 工具)写成 toolchain 模块,见 [CLI 文档](https://cell.cellbang.com/zh/cli)。 # 与 Coding Agent 协作 Cell 的设计目标之一,是让 coding agent(Claude Code、Codex、Cursor、CodeBuddy、pi……)第一次就用对:每件事只有一种写法,错误带结构化的修复建议,命令输出可供机器读取,文档与已安装的版本一致。本页写给配置 agent 的人;agent 自己读的指南是 [AGENTS.md](https://cell.cellbang.com/zh/agent-guide)。 ## 项目里有什么 `cell init` 会写入下列文件;升级 Cell 或增加组件后,`npx cell agent sync` 重写其中由 Cell 管理的部分: | 文件 | 用途 | | --- | --- | | `AGENTS.md` | 已安装的 `@celljs/cli` 版本的 Cell 指南,后面是已安装组件的指南片段。Cell 只重写 `` 和 `` 之间的内容;项目自己的规则写在外面 | | `CLAUDE.md` | `@AGENTS.md`,给 Claude Code | | `.agents/skills/cell/SKILL.md` | 同一份指南,以 agent skill 的形式提供 | | `.mcp.json` | 启动 `npx cell mcp` | 把 `npx cell agent sync` 加进升级流程,或放进 `postinstall` 脚本。 ## MCP `npx cell mcp` 是项目的 stdio MCP 服务器,工具返回与对应命令相同的 JSON: | 工具 | 命令 | 副作用 | | --- | --- | --- | | `info` | `cell info` | 无 | | `check` | `cell check` | 无 | | `config_get` | `cell config get` | 无 | | `docs_search` | `cell docs` | 无 | | `build` | `cell build` | 写输出目录 | | `deploy_plan` | `cell deploy --dry-run` | 写输出目录;可能联网 | 有意不提供部署工具:部署由人执行 `cell deploy`。 ```jsonc // .mcp.json(Claude Code、Cursor、CodeBuddy)或 .cursor/mcp.json { "mcpServers": { "cell": { "command": "npx", "args": ["cell", "mcp"] } } } ``` ```toml # Codex:~/.codex/config.toml [mcp_servers.cell] command = "npx" args = ["cell", "mcp"] ``` `cell dev` 运行时,`http://127.0.0.1:/_cell/mcp` 是运行中应用的第二个 MCP 服务器(HTTP):`get_errors`、`list_routes`、`list_bindings`、`get_config`、`recent_requests` 和 `logs`。它只接受本机的请求。 ## agent 常用的命令 | 命令 | 得到 | | --- | --- | | `npx cell check --json` | 所有类型、配置、装饰器和 DI 问题,带 `code`、`file`、`line` 和 `hint` | | `npx cell info --json` | preset、组件、DI 模块、入口、hooks、产物状态 | | `npx cell config get --why` | 合并后的配置值,以及每一层的来源 | | `npx cell docs <关键词>` | 已安装的包随包发布的文档中的段落(各包 README、这些指南和 CLI 文档) | | `npx cell build --json`、`npx cell deploy --dry-run --json` | 一行结果,而不是进度输出 | 每个命令都接受 `--json`(stdout 上一个带 `schemaVersion` 的 JSON 文档,其余都走 stderr),不在终端里时从不提问,有副作用的命令都有 `--dry-run`。退出码:成功 0,失败 1,用法错误 2。 ## 错误 工具链和运行时的每个错误都有固定的错误码、错误信息、一句话的修复建议和文档链接: ```text [CELL_DI_NO_BINDING] No binding for Symbol(Mailer), needed by Signup.mailer (TodoController → Signup → Symbol(Mailer)). fix: Bind the identifier in a DI module (@Component() in a file the module imports), or mark the injection @Optional(). docs: https://cell.cellbang.com/errors/cell-di-no-binding ``` [错误码列表](https://cell.cellbang.com/zh/errors/)为每个错误码提供一个页面。错误码和 `--json` 字段在 4.x 内保持稳定。 ## 给模型读的文档 文档站发布了 [`llms.txt`](https://cell.cellbang.com/llms.txt)(全部页面的索引)和 [`llms-full.txt`](https://cell.cellbang.com/llms-full.txt)(指南、CLI 文档和各包文档合成一个文件),中文版在 `/zh/llms.txt` 和 `/zh/llms-full.txt`,供从网上读文档的 agent 使用。在项目里则优先用 `cell docs`:它检索的是你安装的版本。 ## 行之有效的习惯 - 让 agent 每次改动后都运行 `npx cell check` 和测试;两者都快,输出也是为它设计的。 - 项目规则写在 `AGENTS.md` 中 Cell 标记之外。 - 让 agent 随意使用 `cell deploy --dry-run`;`cell deploy` 本身及其所需的凭据留给人。 # 组件目录 所有包都发布在 [`@celljs`](https://www.npmjs.com/org/celljs) 下,版本号相同;请让它们保持同一个版本。组件包安装即可使用:它的 DI 模块和 `cell.yml` 会自动加载(见[组件](https://cell.cellbang.com/zh/guide/project#组件))。包名链接到各自的文档。 ## 我需要哪个包? | 我想要 | 添加 | | --- | --- | | 编写 REST 控制器 | `@celljs/mvc`(以 `@celljs/express-adapter` 作为服务端入口;模板里已有) | | 校验请求体 | `@celljs/validation` + `class-validator` | | 发布 OpenAPI / Swagger UI | `@celljs/openapi` | | 从前端以类型安全的方式调用后端 | `@celljs/rpc` | | 构建 React 前端 | `@celljs/react` + `@celljs/serve-static` | | 使用 SQL 数据库 | `@celljs/typeorm` + 驱动(`mysql2`、`pg`、`better-sqlite3`、`sql.js`……) | | 在内存或 Redis 里缓存 | `@celljs/cache` | | 运行定时任务 | `@celljs/schedule` | | 带退避的失败重试 | `@celljs/retry` | | 分离命令、查询和事件 | `@celljs/cqrs` | | 用 winston 记日志 | `@celljs/logger` | | 用户登录、保护路由 | `@celljs/security` | | 签发和验证 JWT | `@celljs/jwt` | | 用 GitHub、Google 或其他 OAuth2 / OIDC 服务登录 | `@celljs/oauth2-client`(配合 `@celljs/security`) | | 调用其他 HTTP API | `@celljs/http`(`RestOperations`) | | 提供或调用 gRPC 服务 | `@celljs/grpc` | | 调用大模型 | `@celljs/ai-core` + `@celljs/ai-pi` | | 构建以自己的服务为工具的 agent | `@celljs/ai-agent` | | 在应用里运行 coding agent | `@celljs/ai-coding-agent` | | 构建终端界面 | `@celljs/tui`(配合 `cli` preset) | | 在阿里云 OSS / 腾讯云 COS 存文件 | `@celljs/oss` / `@celljs/cos` | | 接入微信支付、支付宝或 Stripe | `@celljs/wechat-pay` / `@celljs/alipay` / `@celljs/stripe` | ## 运行时 | 包 | 提供 | | --- | --- | | [`@celljs/core`](https://cell.cellbang.com/zh/packages/core) | DI 容器、`@Component` / `@Autowired` / `@Value`、AOP、配置、应用生命周期、日志、请求上下文。每个应用都依赖它 | | [`@celljs/web`](https://cell.cellbang.com/zh/packages/web) | 基于 `node:http` 的 HTTP 服务:中间件链、请求体解析、cookie、session、CORS、trace ID、健康检查、错误处理器 | | [`@celljs/mvc`](https://cell.cellbang.com/zh/packages/mvc) | 控制器:`@Controller`、`@Get` / `@Post` / ……、参数装饰器、JSON / 文本 / HTML / 文件响应、`@Catch` | | [`@celljs/express-adapter`](https://cell.cellbang.com/zh/packages/express-adapter) | 启动应用并监听 `server.host` / `server.port` 的后端入口。名字虽然如此,并不依赖 express | | [`@celljs/http`](https://cell.cellbang.com/zh/packages/http) | HTTP 客户端(`RestOperations`,基于 `fetch`)和共用的 `HttpStatus`、`HttpHeaders`、`MediaType` | | [`@celljs/validation`](https://cell.cellbang.com/zh/packages/validation) | 用 class-validator / class-transformer 校验控制器和 RPC 参数 | | [`@celljs/openapi`](https://cell.cellbang.com/zh/packages/openapi) | 从控制器生成 OpenAPI 3.0 文档和 Swagger UI | | [`@celljs/rpc`](https://cell.cellbang.com/zh/packages/rpc) | `@Rpc` 服务和类型化代理(`@celljs/react` 的 `useRpc`、`@Autorpc`),JSON-RPC over HTTP | | [`@celljs/serve-static`](https://cell.cellbang.com/zh/packages/serve-static) | 返回前端产物,支持 ETag、Range 和单页应用回退 | | [`@celljs/typeorm`](https://cell.cellbang.com/zh/packages/typeorm) | TypeORM 数据源、`OrmContext` 仓库和 `@Transactional()` | | [`@celljs/cache`](https://cell.cellbang.com/zh/packages/cache) | 基于 cache-manager 的具名缓存(内存、Redis 及其他存储) | | [`@celljs/schedule`](https://cell.cellbang.com/zh/packages/schedule) | `@Scheduler` 类和 `@Cron` 方法 | | [`@celljs/retry`](https://cell.cellbang.com/zh/packages/retry) | 带重试策略和退避策略的重试模板 | | [`@celljs/cqrs`](https://cell.cellbang.com/zh/packages/cqrs) | 命令、查询、事件总线,saga 和聚合 | | [`@celljs/grpc`](https://cell.cellbang.com/zh/packages/grpc) | 基于 `node:http2` 的 gRPC 服务端和客户端,自带 `.proto` 解析,无需代码生成 | | [`@celljs/logger`](https://cell.cellbang.com/zh/packages/logger) | 基于 winston 的 `Logger` | | [`@celljs/security`](https://cell.cellbang.com/zh/packages/security) | 表单登录、HTTP Basic、登出、`@Authenticated()`、`@PreAuthorize()`、策略、密码哈希 | | [`@celljs/jwt`](https://cell.cellbang.com/zh/packages/jwt) | `JwtService`:用 `node:crypto` 签发和验证 JSON Web Token | | [`@celljs/oauth2-core`](https://cell.cellbang.com/zh/packages/oauth2-core) | OAuth2 系列包共用的 OAuth 2.0 / OpenID Connect 类型 | | [`@celljs/oauth2-client`](https://cell.cellbang.com/zh/packages/oauth2-client) | 用 OAuth2 / OIDC 服务登录;client credentials、password、refresh token 授权 | | [`@celljs/oauth2-jose`](https://cell.cellbang.com/zh/packages/oauth2-jose) | OAuth2 系列包用的 JWK 集合与 JWS / JWT 解码 | | [`@celljs/wechat-pay`](https://cell.cellbang.com/zh/packages/wechat-pay) | 微信支付 API v3 客户端和验签的回调通知端点 | | [`@celljs/alipay`](https://cell.cellbang.com/zh/packages/alipay) | 支付宝网关客户端和验签的异步通知端点 | | [`@celljs/stripe`](https://cell.cellbang.com/zh/packages/stripe) | Stripe 客户端和验签的 webhook 端点 | ## 前端 | 包 | 提供 | | --- | --- | | [`@celljs/react`](https://cell.cellbang.com/zh/packages/react) | React 前端:`src/browser/pages` 页面、路由、布局、访问 DI 容器的 hook、应用外壳 | | [`@celljs/widget`](https://cell.cellbang.com/zh/packages/widget) | `@celljs/react` 依赖的前端基础设施:widget、主题、多语言 | | [`@celljs/tui`](https://cell.cellbang.com/zh/packages/tui) | 基于 pi-tui 的终端界面,含 agent 对话 | ## AI | 包 | 提供 | | --- | --- | | [`@celljs/ai-core`](https://cell.cellbang.com/zh/packages/ai-core) | 与服务商无关的 `ChatModel`、`EmbeddingModel`、`PromptTemplate`,以及负责路由的 `ProxyChatModel` | | [`@celljs/ai-pi`](https://cell.cellbang.com/zh/packages/ai-pi) | 经 pi-ai 接入 20 多家模型服务商,写作 `pi:/` | | [`@celljs/ai-agent`](https://cell.cellbang.com/zh/packages/ai-agent) | 以 Cell 组件为工具的 agent;事件经 SSE 推送 | | [`@celljs/ai-coding-agent`](https://cell.cellbang.com/zh/packages/ai-coding-agent) | 由 DI 管理的 pi coding agent 会话 | ## 云 | 包 | 提供 | | --- | --- | | [`@celljs/cloud`](https://cell.cellbang.com/zh/packages/cloud) | 共用的云抽象:凭据、地域、账号,以及 `ObjectStorageService` 接口 | | [`@celljs/oss`](https://cell.cellbang.com/zh/packages/oss) | 基于阿里云 OSS 的 `ObjectStorageService` | | [`@celljs/cos`](https://cell.cellbang.com/zh/packages/cos) | 基于腾讯云 COS 的 `ObjectStorageService` | 部署到函数计算、SCF、Lambda、Vercel 或 Cloudflare 不需要额外的包:选择 preset 即可([构建与部署](https://cell.cellbang.com/zh/guide/deployment))。 ## 工具链 | 包 | 提供 | | --- | --- | | [`@celljs/cli`](https://cell.cellbang.com/zh/cli) | `cell` 命令、preset、部署器、`createTestApp()`、这些指南 | | [`@celljs/vite`](https://cell.cellbang.com/zh/packages/vite) | `cell build` 和 `cell dev` 背后的 Vite 插件与 `CellBuilder` | | [`@celljs/kit`](https://cell.cellbang.com/zh/packages/kit) | 给 toolchain 模块和 preset 作者的共享类型:`CellError`、事件、`definePreset` | # @celljs/cli Cell 4 的命令行工具 `cell`:读取 `cell.config.ts`,按 preset 构建应用,运行和部署构建产物。入门和各主题的用法见[指南](https://cell.cellbang.com/zh/guide/getting-started)(随本包发布在 `docs/`,`cell docs` 可以检索)。设计见仓库的 [docs/design/architecture.md](https://github.com/cellbang/cell/blob/main/docs/design/architecture.md),从 3.x 迁移见 [docs/migration-4.md](https://github.com/cellbang/cell/blob/main/docs/migration-4.md)。 需要 Node.js `^20.19.0 || >=22.12.0`。在项目里安装: ```bash npm install -D @celljs/cli npx cell build ``` ## 命令 | 命令 | 作用 | | --- | --- | | `cell init [template] [dir]` | 从模板创建项目(`backend`、`fullstack`),自带 AGENTS.md、cell 技能和 `.mcp.json`;不带参数时列出模板 | | `cell dev` | 开发:前后端同一个端口,前端 HMR,后端改文件自动重载,配置变化自动重启 | | `cell build` | 构建到 `.cell/output`:`server/index.mjs`(依赖内联的单个文件)、`public/`(前端)、`manifest.json` | | `cell info` | 描述项目:preset 及其来源、mode、目标、组件、DI 模块、hooks、产物状态。只读,不构建、不执行 hooks | | `cell check` | 一次完成配置、TypeScript 类型、装饰器设置、后端 DI 绑定和模块注册的检查,有错误时退出码 1 | | `cell preview` | 按 `manifest.json` 运行构建产物,不重新构建 | | `cell deploy` | 用 preset 构建,再交给 preset 的部署器发布;`--dry-run` 只给出计划 | | `cell config get ` | 某个配置项合并后的值,`--why` 列出设置它的文件和环境变量 | | `cell docs ` | 检索已安装组件和模块随包的文档,以及 `@celljs/cli` 自带的指南与本文档(与项目所用版本一致) | | `cell mcp` | stdio MCP 服务器,把 `info`、`check`、`config_get`、`docs_search`、`build`、`deploy_plan` 和模块注册的工具提供给 coding agent | | `cell agent sync` | 按已安装的版本重写 AGENTS.md 中由 Cell 管理的部分和 `.agents/skills/cell/SKILL.md` | ## Preset | preset | 产物 | 运行 / 预览 | 部署器 | | --- | --- | --- | --- | | `node`(默认) | Node.js HTTP 服务:`node .cell/output/server/index.mjs` | `cell preview` 用 Node.js 运行 | 无,产物自行部署 | | `cloudflare` | Cloudflare Workers:`server/worker.mjs` + `wrangler.json`,前端作为静态资源 | `cell preview` 用 `wrangler dev` 在本机的 workerd 中运行 | `cloudflare`(Wrangler) | | `cli` | Node.js 命令行程序:`node .cell/output/server/index.mjs` 启动应用但不起服务,由组件(如 `ApplicationLifecycle`)完成工作;不需要 `@celljs/express-adapter`,也不报缺少入口 | `cell preview` 用 Node.js 运行,继承当前终端 | 无 | | `fc` | 阿里云函数计算 3.0 自定义运行时:产物加 `bootstrap` 启动脚本,前端由服务端从 `public/` 返回 | Node.js 运行 | `fc`(函数计算 API) | | `scf` | 腾讯云 SCF Web 函数:产物加 `scf_bootstrap` | Node.js 运行 | `scf`(云函数 API) | | `lambda` | AWS Lambda + 函数 URL:产物加 `server/lambda.mjs` 处理器 | Node.js 运行 | `lambda`(AWS API) | | `vercel` | Vercel Build Output API:`.vercel/output`,前端在 CDN,服务端是一个函数 | Node.js 运行函数的 bundle,前端取自 `static/` | `vercel`(Vercel CLI) | 每个 preset 的选项写在 `cell.config.ts` 里与 preset 同名的字段(如 `fc: { region: 'cn-shanghai' }`),按 preset 的 schema 校验,未知选项报 `CELL_CONFIG_INVALID`;切换 preset 时其他 preset 的字段保留不报错。 ### cloudflare ```bash cell build --preset cloudflare # 构建到 .cell/output cell preview # wrangler dev,在 workerd(Cloudflare 的运行时)中运行,默认 8787 端口 cell deploy --preset cloudflare --dry-run # 构建并执行 wrangler deploy --dry-run,输出计划,不上传 cell deploy --preset cloudflare # 构建并部署,输出 workers.dev 地址 ``` - **运行方式**:Cell 后端运行在 Workers 的 `node:http` 服务端上(需要 `nodejs_compat`),由 `@celljs/express-adapter` 的 `http` 入口提供,与 `node` preset 相同。前端(`public/`)由 Cloudflare 作为静态资源直接返回,只有不匹配静态文件的请求进入 Worker。产物只支持 ESM,`--format cjs` 报 `CELL_CONFIG_CONFLICT`。 - **启动时机**:Workers 只允许在模块加载阶段生成代码(`new Function`,部分依赖会用到),只允许在处理请求时做 I/O。所以应用的模块在 Worker 启动时加载,`Application.start()` 和 `onStart` 在第一个请求时执行(`ApplicationHost.beforeStart`)。模块加载失败会让 Worker 启动失败,`wrangler dev` / `wrangler deploy` 直接报出。 - **Cloudflare 的配置写在项目根目录的 `wrangler.jsonc` 或 `wrangler.json`**(可选,按 Cloudflare 文档的写法):`name`、`compatibility_date`、`vars`、KV / D1 / R2 等绑定、`routes` 都在这里。构建时 Cell 以它为基础生成 `.cell/output/wrangler.json`,并设置:`main`(项目里写了会被忽略并警告)、有前端时的 `assets.directory`、`compatibility_flags` 里的 `nodejs_compat`。没有配置文件时,`name` 取自 package.json 的包名(如 `@acme/shop` → `acme-shop`),`compatibility_date` 默认 `2026-03-17`,这也是允许的最早日期(`node:http` 服务端与 `node:tty` 从这天起默认可用)。`wrangler.toml` 不支持,请改写成 `wrangler.jsonc`。 - **单页应用**:有前端时默认 `assets.not_found_handling: "single-page-application"`:浏览器导航到不存在的路径时由 Cloudflare 返回 `index.html`(与 Node.js 上 `@celljs/serve-static` 的 `spa` 一致),API 等其他请求仍进入 Worker;在 `wrangler.jsonc` 里写了 `not_found_handling` 就以项目为准。Worker 里没有前端文件,`@celljs/serve-static` 发现没有前端目录时直接放行,未知路径得到正常的 404。 - **自定义域名**:域名的 DNS 托管在 Cloudflare 时,在 `wrangler.jsonc` 里写 `"routes": [{ "pattern": "app.example.com", "custom_domain": true }]`,部署时 Cloudflare 自动创建 DNS 记录和证书,`deployment.url` 报告这个域名。国内网络无法访问 `*.workers.dev`,面向国内访问时要绑定自定义域名。 - **应用配置**:`vars` 和 secrets(`wrangler secret put`)进入 `process.env`,所以 `cell.yml` 里的 `${env.X}` 和 `CELL_*` 变量照常生效。 - **Wrangler**:不是 Cell 的依赖。项目装了 `wrangler` 就用项目的,否则由 `npx --yes wrangler@4` 在第一次使用时安装(npm 缓存)。凭据由 Wrangler 管理:设置 `CLOUDFLARE_API_TOKEN`(必要时加 `CLOUDFLARE_ACCOUNT_ID`),或执行一次 `npx wrangler login`。也可以不用 `cell deploy`,直接执行 `npx wrangler deploy --config .cell/output/wrangler.json`。 - **Workers Builds**(Cloudflare 的 CI,设置 `WORKERS_CI=1`)里自动选用 `cloudflare` preset:构建命令 `npx cell build`,部署命令 `npx cell deploy --skip-build`。 - `wrangler dev` 会在项目根目录创建 `.wrangler/`,请加入 `.gitignore`。 ### fc、scf、lambda 三个函数平台的做法相同:Cell 服务端原样运行,产物目录整体打成 zip 作为函数代码;部署器直接调用云 API(请求签名自己实现:阿里云 ACS3、腾讯云 TC3、AWS SigV4),不需要云 SDK 或云厂商 CLI。函数名默认取包名(`@acme/shop` → `shop`),可用 `name` 指定;`env` 写入函数的环境变量(另带 `NODE_ENV=production`),`cell.yml` 里的 `${env.X}` 和 `CELL_*` 照常生效。 | preset | 运行方式 | 选项 | 凭据(环境变量) | 部署结果 | | --- | --- | --- | --- | --- | | `fc` | `custom.debian10` 运行时,`bootstrap` 用镜像自带的 Node.js 20(`node: '18'` 可选)在 9000 端口(`FC_SERVER_PORT`)启动服务 | `name`、`region`(默认 `ALIBABA_CLOUD_REGION_ID` 或 `cn-hangzhou`)、`node`、`memory`(MB,默认 1024)、`cpu`(默认按内存 2 GB/核)、`timeout`(默认 60)、`concurrency`(单实例并发,默认 20)、`env` | `ALIBABA_CLOUD_ACCESS_KEY_ID`、`ALIBABA_CLOUD_ACCESS_KEY_SECRET`(可选 `ALIBABA_CLOUD_SECURITY_TOKEN`) | 创建或更新函数,没有时创建匿名 HTTP 触发器 `http`(已有的触发器不改),返回 `fcapp.run` 地址 | | `scf` | Web 函数(`Type: HTTP`),`scf_bootstrap` 用运行时的 Node.js 18(`runtime: 'Nodejs16.13'` 可选)在 9000 端口启动服务;只有 `/tmp` 可写 | `name`、`region`(默认 `TENCENTCLOUD_REGION` 或 `ap-guangzhou`)、`namespace`(默认 `default`)、`runtime`、`memory`(默认 512)、`timeout`(默认 60)、`env` | `TENCENTCLOUD_SECRET_ID`、`TENCENTCLOUD_SECRET_KEY`(可选 `TENCENTCLOUD_SESSION_TOKEN`) | 创建或更新函数(先代码后配置,等待状态变为 Active),没有时创建公网、免鉴权的函数 URL | | `lambda` | `nodejs22.x`,初始化阶段在回环端口启动服务,`server/lambda.handler` 把函数 URL / HTTP API(payload 2.0)事件转成请求,响应以 base64 返回,多个 Cookie 放在 `cookies` | `name`、`region`(默认 `AWS_REGION` 或 `us-east-1`)、`runtime`、`architecture`(`x86_64` / `arm64`)、`memory`(默认 1024)、`timeout`(默认 30)、`role`、`env` | `AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY`(可选 `AWS_SESSION_TOKEN`),或 `~/.aws/credentials` 的 `AWS_PROFILE` / default | 没有 `role` 时创建或复用执行角色 `cell-lambda-execution`(只有日志权限);创建或更新函数;没有时创建 `AuthType: NONE` 的函数 URL 及其两项公开调用权限 | - 代码包直接随请求上传,上限:FC 100 MB、SCF 20 MB、Lambda 50 MB(zip 后),超出报 `CELL_DEPLOY_PACKAGE_TOO_LARGE`。体积大多来自 `build.external` 留在 `server/node_modules` 的包。 - 没有凭据时 `--dry-run` 照样给出计划(动作为 `deploy`,并警告不知道函数是否已存在);有凭据时查询云端,动作是 `create` 或 `update`。实际部署缺凭据报 `CELL_DEPLOY_CREDENTIALS_MISSING`,云 API 的错误报 `CELL_DEPLOY_API_ERROR` / `CELL_DEPLOY_UNAUTHORIZED`,消息里带云厂商的错误码和请求 ID。 - 函数计算的默认域名 `fcapp.run` 会让浏览器下载 HTML 页面,网页应用请绑定自定义域名;API 不受影响。 - 镜像里的 Node.js 较旧(FC 20.10、SCF 18.15):服务端 bundle 自带 `import.meta.dirname` / `filename` 的补丁。 ### vercel ```bash cell build --preset vercel # .vercel/output(Build Output API v3) cell preview # Node.js 运行函数的 bundle,前端取自 .vercel/output/static cell deploy --preset vercel # vercel deploy --prebuilt,不在 Vercel 上再构建 ``` - 前端放在 `static/` 由 Vercel CDN 返回(`/assets/*` 带一年的 immutable 缓存头);其他请求进入函数 `functions/index.func`(`nodejs22.x`),函数里的 Cell 服务端处理 API,并用其中的 `public/index.html` 做单页回退。`vercel build` 留在 `.vercel/output` 的文件在构建时清除。 - 选项:`project`(默认取包名)、`scope`(团队)、`environment`(`production` 默认 / `preview`)、`runtime`、`memory`、`maxDuration`、`regions`(逗号分隔)。输出目录固定 `.vercel/output`(`--out-dir` 到别处时部署报 `CELL_DEPLOY_OUTPUT_MISMATCH`);不接受 `--single` / `--compile`。 - Vercel CLI 不是依赖:项目装了就用项目的,否则 `npx --yes vercel@65`。凭据:`VERCEL_TOKEN`,或执行一次 `npx vercel login`;项目不存在时自动创建(`--create-if-missing`)。 - Vercel 的构建环境(`VERCEL=1`)里自动选用 `vercel` preset。`.vercel/` 请加入 `.gitignore`。 ### cell dev | 选项 | 说明 | | --- | --- | | `--preset ` | 用哪个 preset 的入口运行后端,默认 `node` | | `-m, --mode ` | 额外的 mode,可重复;`development` 总是自动加上 | | `-p, --port ` | 端口,默认取 `server.port`(考虑 `CELL_SERVER__PORT`),否则 3000;`0` 表示随机端口 | | `--host ` | 监听地址,默认取 `server.host`,否则 `127.0.0.1` | - 一个端口:Vite 提供前端页面和模块(HMR),`/favicon.ico`、组件的 `assets` 与 `public/` 里的文件和构建产物里的位置一致,其余请求交给后端。 - 后端不打包,在 Vite 的 server 环境里由 module runner 加载:Cell 包和项目源码经过 Cell 插件(生成的 DI 容器与配置写到 `.cell/generated/`),其他依赖由 Node 直接加载。入口通过 `ApplicationHost`(`@celljs/core`)把请求处理函数交给 `cell dev`,不自己监听端口。 - 后端用到的文件变化时:先停止旧应用(触发 `onStop`),再从新的模块重新启动,并触发 `dev:reload`。启动失败时请求返回 500 和 `CellError`(`CELL_DEV_BACKEND_FAILED`),保存后自动重试。 - 项目或组件的 `cell*.yml`、`package.json`、`cell.config.*` 变化时:重新解析工具链并在同一端口重启,再次触发 `dev:ready`。配置写错时报错并等待下一次修改。 - 源码里的 `require()` 和项目内的 CommonJS 文件(如生成的 prisma client)照常可用。 - `POST /_cell/mcp`:运行中应用的 MCP 端点(Streamable HTTP,JSON 响应),工具有 `get_errors`(后端启动错误、失败的重载、5xx 请求、错误日志)、`list_routes`、`list_bindings`、`get_config`(`${...}` 已求值)、`recent_requests`、`logs`。只接受本机来源(无 `Origin` 或回环地址),其他来源返回 403。 - `--json`:就绪时 stdout 输出一行结果(`url`、`port`、`mode`、`targets`、`mcp`,后端启动失败时带 `backendError`),之后应用和 Vite 的输出都走 stderr。收到 SIGINT / SIGTERM 时停止应用并以 0 退出。 - React Fast Refresh 等框架插件由组件的 toolchain 模块提供(模块宿主完成后);在那之前前端修改会整页刷新,CSS 热替换。 ### cell build | 选项 | 说明 | | --- | --- | | `--preset ` | 产物形态:`node`(默认)、`cloudflare`、`cli`、`fc`、`scf`、`lambda`、`vercel` | | `-m, --mode ` | 额外的 mode,读取 `cell..yml`,可重复;`production` 总是自动加上 | | `--format ` | 服务端产物格式,默认取 preset(`node` 为 `esm`) | | `--single` | 前端内嵌进 `server/index.mjs`:一个文件运行整个应用(见下) | | `--compile [bun\|node\|deno]` | 编译成一个可执行文件 `.cell/output/app`,默认 bun(见下) | | `--compile-target ` | 交叉编译,交给 bun(如 `bun-linux-x64`)或 deno(如 `x86_64-unknown-linux-gnu`) | | `--sourcemap` | 生成 source map | | `--out-dir ` | 输出目录,默认 `.cell/output` | **前端的静态文件**:构建前端后复制进 `public/`,不覆盖构建产物:项目根目录的 `favicon.ico`;各组件(含项目自己)`public/` 或 `src/browser/public/` 里的文件,原样放在站点根路径(Web 应用清单、Service Worker、`robots.txt` 等),同名时项目自己的优先;都没有 favicon 时用 Cell 的;各组件 `assets/`、`src/assets/`、`src/browser/assets/` 里的文件放在 `/assets/` 下。 **单文件(`--single`)**:先构建前端,再把 `public/` 的全部文件(页面、脚本、favicon、组件的 assets)以 base64 写进服务端 bundle,启动时注册到 `Symbol.for('@celljs/serve-static/assets')`,`@celljs/serve-static` 从内存返回它们(Content-Type、ETag / 304、Cache-Control 与从磁盘时一致,单页应用回退到 `index.html`)。产物只有 `server/index.mjs` 和 `manifest.json`(`single: true`,没有 `public`),把这一个文件拷到任意目录用 `node index.mjs` 即可运行。项目只有后端时与不加 `--single` 相同(会提示);`build.external` 等留在 `server/node_modules` 的包仍要放在它旁边。也可在 `cell.config.ts` 写 `build: { single: true }`。 **可执行文件(`--compile`)**:在单文件 bundle 的基础上编译成 `.cell/output/app`(Windows 目标为 `app.exe`),拷到没有 Node.js 和 node_modules 的机器也能运行;`manifest.json` 记录 `executable`,`cell preview` 直接运行它。 | 工具 | 做法 | 需要 | | --- | --- | --- | | `bun`(默认) | `bun build --compile` | 本机装有 Bun | | `deno` | `deno compile --allow-all` | 本机装有 Deno | | `node` | Node.js 单可执行应用(SEA):CommonJS bundle 生成 blob,用 `npx postject` 注入 node 的副本,macOS 上重新签名 | 产物格式自动为 `cjs`;第一次使用时 npx 下载 postject;不能交叉编译 | 留在 bundle 外的包(`build.external`、组件的 `optionalDependencies`,通常是原生模块)无法放进可执行文件,报 `CELL_COMPILE_EXTERNALS` 并列出它们。`cloudflare`、`vercel` preset 的前端由平台托管,不接受 `--single` / `--compile`。 ### cell info 接受与 `cell build` 相同的 `--preset`、`-m`、`--format`、`--out-dir` 等选项,描述同样参数下 `cell build` 会用到的内容;另有: | 选项 | 说明 | | --- | --- | | `--app-config` | 同时输出每个目标的应用配置(`cell.yml`):后端是全部键,`${...}` 保持原样、运行时才求值;前端只有 public 键,已求值 | 只读取 package.json、`cell*.yml`、`cell.config.ts` 和 `manifest.json`,不运行项目代码,不执行 `hooks`。`--json` 结果的主要字段: | 字段 | 内容 | | --- | --- | | `name`、`version`、`cellVersion`、`configFile` | 项目、CLI 版本、`cell.config` 文件(相对根目录) | | `preset` | `name`、`source`(`flag` / `config` / `env` / `ci` / `default`)、`entry`、`format`、`runtime`(`workerd` 表示 Cloudflare Workers)、`deployer`、`available` | | `mode`、`targets`、`targetSources` | 生效的 mode;构建目标及其来由(`cell.config.ts`、`default` 或声明它的组件) | | `build` | 生效的 `format`、`outDir`、`external`、`sourcemap`、`single`、`compile` | | `components` | 按加载顺序;`dir`、`configFiles` 相对根目录,`targets` / `entries` / `toolchain` / `optionalDependencies` 只在组件声明时出现 | | `frontend`、`backend` | `entry`、提供入口的 `entryComponent`、`staticModules` / `modules`(DI 模块,按加载顺序)、`config`(仅 `--app-config`);前端有页面时还有 `routes`(`src/browser/pages` 的 `path`、`file`、`layouts`,有 `_error` 文件时还有 `error`) | | `hooks`、`modules`、`moduleOptions` | `cell.config.ts` 里的 hooks、`modules` 条目、toolchain 模块选项的键 | | `output` | `dir`、`built`,构建过时还有 manifest 里的 `preset`、`format`、`targets`、`server`、`public` | | `frontend.envOverrides`、`backend.envOverrides` | 当前环境里的 `CELL_*` / `CELL_*` 变量:`applied`(变量 → 配置路径)和 `problems`(找不到键或类型不符)。没有这类变量时不出现,不输出值 | `warnings` 会提前指出不起作用或会让 `cell build` 失败的设置:没有组件提供 preset 的入口、`-m` 指定的 mode 没有任何 `cell..yml`(附拼写建议)、不生效的 `CELL_*` / `CELL_*` 变量。 ### cell check | 选项 | 说明 | | --- | --- | | `--preset `、`-m, --mode ` | 同 `cell build` | | `--skip ` | 跳过某项检查,可重复;未知名字报 `CELL_ARGUMENT_INVALID` 并列出可用的检查 | | 检查 | 内容 | | --- | --- | | `config` | 与 `cell info` 的 warnings 相同:缺少入口(`CELL_ENTRY_MISSING`)、无效 mode(`CELL_MODE_UNKNOWN`)、不生效或被遮住的环境变量(`CELL_ENV_OVERRIDE_IGNORED` / `CELL_ENV_OVERRIDE_SHADOWS`) | | `types` | 用项目的 TypeScript 执行 `tsc --noEmit -p tsconfig.json`,每条诊断带文件、行列和 `TSxxxx` 码;没有 tsconfig.json 或 typescript 时跳过 | | `decorators` | tsconfig 要开启 `experimentalDecorators` 和 `emitDecoratorMetadata`(`CELL_TSCONFIG_DECORATORS`),与构建时的装饰器语义一致 | | `di` | 用 `cell dev` 的方式加载后端 DI 容器(不启动应用),逐个解析绑定:容器抛出的 `CELL_DI_NO_BINDING`、`CELL_DI_AMBIGUOUS`、`CELL_DI_CIRCULAR`、`CELL_DI_MISSING_INJECT`、`CELL_DI_INCOMPLETE_BINDING` 为错误,信息里有谁需要它和解析链,带修复建议;同一个原因只报一次;只能在注入点或请求中创建的绑定记为 info | | `security` | 后端 `SecurityCheck` 组件报告的生产环境不安全设置,均为 warning(不让检查失败),与生产启动时 `securityChecks` 处理的是同一批:`CELL_SECURITY_DEFAULT_COOKIE_KEYS`、`CELL_SECURITY_COOKIE_KEYS_MISSING`、`CELL_SECURITY_WEAK_COOKIE_KEYS`(`@celljs/web`)、`CELL_SECURITY_WEAK_JWT_SECRET`(`@celljs/jwt`);读取的是所选 mode 的配置。与 `di` 共用一次后端加载 | | 模块注册的检查 | toolchain 模块用 `addCheck` 注册,名字如 `typeorm:entities` | `--json` 结果:`passed`、`summary`(errors / warnings / info 计数)、`issues`(每条带 `check`、`level`、`code`、`message`,可能有 `file`、`line`、`column`、`hint`)、`checks`(每项的耗时、问题数、跳过原因)。不加 `--json` 时按编译器格式输出 `src/a.ts:3:5 error TS2322 …`,info 不显示。 ### cell init ```bash npx @celljs/cli init backend my-app # 或 fullstack:React 前端 + 后端,经 RPC 调用 cd my-app && npm install && npx cell agent sync && npx cell dev ``` | 选项 | 说明 | | --- | --- | | `--name ` | 包名,默认取目录名(转成小写、去掉空格) | | `--force` | 允许写入非空目录(同名文件被覆盖) | 从不提问;目录非空时报 `CELL_INIT_NOT_EMPTY`,模板不存在时报 `CELL_TEMPLATE_NOT_FOUND`(带拼写建议)。项目依赖 `@celljs/*` 的 `^`,测试用 `bun test` + `createTestApp()`。为 coding agent 生成:`AGENTS.md`(Cell 指南,与版本匹配)、`.agents/skills/cell/SKILL.md`、`CLAUDE.md`(`@AGENTS.md`)、`.mcp.json`(`npx cell mcp`)。 ### cell agent sync 把 `@celljs/cli` 自带的 Cell 指南和已安装组件、toolchain 模块(`addDocs` 目录)的 `AGENTS.md` 片段写进 `AGENTS.md` 的 `` … `` 之间(其余内容保持不变;没有标记时追加到末尾),并重写 `.agents/skills/cell/SKILL.md`。升级 Cell 或增减组件后执行一次。 ### cell config get ```bash cell config get server.port --why # server.port (backend) = 8080 # server-only, owned by @celljs/web # Port the server listens on. Override with CELL_SERVER__PORT. # node_modules/@celljs/web/cell.yml: 3000 # cell.yml: 8080 ``` | 选项 | 说明 | | --- | --- | | `-m, --mode ` | 额外的 mode,可重复 | | `--target ` | `backend` 或 `frontend`(只有 public 键),默认有后端时取 `backend` | | `--why` | 列出该键的归属组件、是否进入前端、说明,以及按合并顺序设置它的文件和环境变量 | 键可写 `a.b.c`、`items[0].name`。`--json` 结果有 `value`、`owners`(在 `cell.schema.json` 里声明该键的组件)、`public`(前端是否拿到)、`description`、`layers`(`file`、`component`、`value`)、`env`(设置了该键或其上下级的 `CELL_*` 变量)、`expressions`(后端值含 `${...}`,服务端启动时求值)。键不存在时报 `CELL_CONFIG_KEY_NOT_FOUND`,带最接近的键;用 `--target frontend` 查服务端独有的键时说明它是 server-only。 ### cell docs `cell docs [--limit n]`:在各组件的 `README.md`、`docs/`,模块 `addDocs()` 的目录,以及 `@celljs/cli` 自带的本文档和指南(`docs/`:快速开始、配置、依赖注入、HTTP、RPC、测试、部署、coding agent、组件目录,英文)里按标题分段检索,返回最匹配的段落和 `file:line`(`source` 为 `@celljs/cli` 的来自 CLI)。 ### cell mcp 在 stdio 上运行 MCP 服务器(每行一条 JSON-RPC 消息),直到 stdin 关闭。工具都对应同名命令的 `--json` 结果: | 工具 | 对应命令 | 副作用 | | --- | --- | --- | | `info` | `cell info` | 只读 | | `check` | `cell check` | 只读 | | `config_get` | `cell config get` | 只读 | | `docs_search` | `cell docs` | 只读 | | `build` | `cell build` | 写输出目录 | | `deploy_plan` | `cell deploy --dry-run` | 写输出目录,部署器可能联网;不部署 | 不提供部署工具:实际部署由人执行 `cell deploy`。 之后是 toolchain 模块用 `addMcpTool` 注册的工具(与内置工具重名的会被忽略并警告);`cell.config.*`、package.json 或模块文件变化后自动重新加载。每个工具标注 `readOnlyHint` / `openWorldHint`;参数按 `inputSchema` 校验,失败时返回 `isError` 和 `CellError`。stdout 只有协议消息,模块和 hooks 的输出都走 stderr。 在 agent 中配置(项目根目录下运行): ```jsonc // Claude Code / Cursor / CodeBuddy:.mcp.json 或 .cursor/mcp.json { "mcpServers": { "cell": { "command": "npx", "args": ["cell", "mcp"] } } } ``` ```toml # Codex:~/.codex/config.toml [mcp_servers.cell] command = "npx" args = ["cell", "mcp"] ``` `cell dev` 运行时,另外把 `http://127.0.0.1:/_cell/mcp` 配成 HTTP 类型的 MCP 服务器即可查看运行中的应用。 ### cell preview | 选项 | 说明 | | --- | --- | | `-p, --port ` | 监听端口,默认取 `server.port`(3000);等同于设置 `CELL_SERVER__PORT`。`cloudflare` 产物传给 `wrangler dev --port`,默认 8787 | | `--out-dir ` | 产物目录,默认 `.cell/output` | 产物由哪个 preset 构建,就按它的方式运行:`node`、`cli`、`fc`、`scf`、`lambda` 用 Node.js 运行 `server/index.mjs`(标准输入输出继承当前终端),`cloudflare` 用 `wrangler dev`,`vercel` 用 Node.js 运行函数里的 bundle 并从 `static/` 返回前端。 ### cell deploy | 选项 | 说明 | | --- | --- | | `--preset ` | 部署到哪个平台:`cloudflare`、`fc`、`scf`、`lambda`、`vercel`;`node`、`cli` 没有部署器,报 `CELL_DEPLOY_UNSUPPORTED`(构建之前就检查) | | `--dry-run` | 构建并计划,不上传(`cloudflare`:`wrangler deploy --dry-run`;`fc` / `scf` / `lambda`:打包并查询函数是否存在;`vercel`:列出函数和静态文件) | | `--skip-build` | 部署 `--out-dir` 里已有的产物;产物的 preset 与 `--preset` 不一致时报 `CELL_DEPLOY_OUTPUT_MISMATCH` | | `-m, --mode `、`--sourcemap`、`--out-dir ` | 同 `cell build` | 先 `plan` 后 `apply`:计划列出要部署的资源(`resources`,每项有 `type`、`name`、`action`、`details`),`--dry-run` 到此为止。依次触发 `deploy:before`(闸门,hooks 里的命令失败会取消部署)、`deploy:plan`、`deploy:done` 事件。部署工具(Wrangler、Vercel CLI)的输出在 `--json` 时走 stderr。`--json` 结果:`preset`、`deployer`、`dryRun`、`plan`,实际部署时还有 `deployment`(`url`,以及 `details.versionId`)。 ### 通用选项 | 选项 | 说明 | | --- | --- | | `--root ` | 项目根目录,默认当前目录 | | `--json` | stdout 只输出一行 JSON 结果,其余输出(进度、hook 命令的输出)走 stderr | | `-h, --help` / `-v, --version` | 帮助 / 版本 | 命令从不交互提问。退出码:成功 0,执行失败 1,用法错误(未知命令、选项、取值)2。 ## --json 输出 ```json {"schemaVersion":1,"command":"build","ok":true,"root":"/app","preset":"node","format":"esm","mode":["production"],"targets":["backend"],"outDir":".cell/output","entry":"@celljs/express-adapter/lib/node/http-application-entry","durationMs":480,"warnings":[]} ``` 失败时: ```json {"schemaVersion":1,"command":"build","ok":false,"error":{"code":"CELL_PRESET_NOT_FOUND","message":"Unknown preset 'nod'. Available: node.","hint":"Did you mean 'node'?","docs":"https://cell.cellbang.com/errors/cell-preset-not-found"},"warnings":[]} ``` `schemaVersion` 不变时,字段只增不改。 ## cell.config.ts 可选。只放工具链配置;应用配置(`@Value` 读取的内容)写在 `cell.yml`。 ```ts import { defineConfig } from '@celljs/cli'; export default defineConfig({ preset: 'node', targets: ['frontend', 'backend'], modules: ['!@celljs/serve-static', { source: '@x/ui', runtime: false }], build: { format: 'esm', external: ['sharp'], sourcemap: false, outDir: '.cell/output' }, vite: {}, hooks: { 'build:before': 'prisma generate' }, }); ``` | 字段 | 说明 | | --- | --- | | `preset` | 产物形态。优先级:`--preset` → 本字段 → `CELL_PRESET` → CI 环境识别 → `node` | | `targets` | 构建目标。默认 `backend` 加上组件声明的目标(装了 `@celljs/react` 会经 `@celljs/widget` 加上 `frontend`);写了就完全以它为准 | | `modules` | `'!包名'` 去掉一个组件(连同只经由它引入的依赖);`{ source, runtime: false }` 去掉它的 DI 模块、保留配置;`{ source, toolchain: false }` 去掉它的 toolchain 模块;本地路径或包名表示额外加载的 toolchain 模块(见下文) | | `build.format` | `esm` 或 `cjs` | | `build.external` | 不内联的包,按依赖关系拷到 `server/node_modules` | | `build.sourcemap`、`build.outDir` | 同命令行选项 | | `build.single`、`build.compile` | 同 `--single` / `--compile`(`compile` 可写 `true` 或 `bun` / `node` / `deno`) | | `vite` | 合并进每个环境的 Vite 配置 | | `hooks` | 在工具链事件上执行的 shell 命令,见下文 | 其他顶层字段是 toolchain 模块的选项,值必须是对象。文件支持 `.ts/.mts/.cts/.js/.mjs/.cjs`,同时存在多个报 `CELL_CONFIG_AMBIGUOUS`。命令行参数优先于配置文件。 ## 用环境变量覆盖配置 `cell.yml` 里已有的任意键都可以用环境变量覆盖,不用改文件: ```bash CELL_SERVER__PORT=8080 cell preview # 服务端启动时应用:server.port CELL_APP__PUBLIC__API_BASE=/api cell build # 构建时应用,进入前端:app.public.apiBase ``` 前缀后只有一个下划线,层级之间用 `__`(`CELL_SERVER__PORT`)。`CELL_PRESET`、`CELL_MODE`、`CELL_EVENT`、`CELL_OUT_DIR`、`CELL_DEBUG` 是工具链自己的变量,不当作配置;只有一段、又不是顶层键的变量(如别的工具的 `CELL_REGION`)直接忽略;带 `__` 却找不到键的会报告,写成旧的 `CELL__…` / `CELL_BACKEND__…` 时提示正确写法。`__` 分隔层级,一个键只能由一个变量设置,不区分大小写并忽略 `_`、`-`;值按 schema 类型或原值类型转换(JSON 写法的有限数、`true` / `false`、对象和数组用 JSON,原值为 `null` 时是 JSON 就按 JSON),整体是一个 `${...}` 的值保留到求值时再定类型(如 `CELL_SERVER__PORT='${env.PORT}'`),在 `${...}` 求值之前生效。只能覆盖已存在的键,找不到键或类型不符的变量不生效并给出警告:服务端在启动时打印,`cell build` 和 `cell info` 也会检查。完整规则见 [docs/design/config.md](https://github.com/cellbang/cell/blob/main/docs/design/config.md)。 ## hooks ```ts hooks: { 'build:before': 'prisma generate', 'output:done': 'cp extra/* "$CELL_OUT_DIR/server/"', } ``` - 命令经 shell 在项目根目录执行,不读标准输入。PATH 最前面是项目及上级目录的 `node_modules/.bin`。 - 环境变量:`CELL_EVENT`、`CELL_PRESET`、`CELL_MODE`(逗号分隔)、`CELL_OUT_DIR`(输出目录的绝对路径)。 - `cell dev` 触发 `modules:done` → `config:resolved` → `dev:ready`,后端重载时 `dev:reload`,重启时 `close` 后重新来一遍。 - `cell build` 依次触发:`modules:done` → `config:resolved` → `build:before` → 构建 → `output:done` → `close`。 - `build:before` 是闸门事件:命令非零退出即取消构建(`CELL_BUILD_CANCELLED`)。其他事件里非零退出报 `CELL_HOOK_FAILED`。 - 只接受命令字符串。需要写代码或修改事件对象(`build:done`、`deploy:plan`)的扩展写成 toolchain 模块。 ## toolchain 模块 需要写代码的扩展写成 toolchain 模块:默认导出一个工厂函数,拿到 `cell` 后注册能力(设计见 [docs/design/modules.md](https://github.com/cellbang/cell/blob/main/docs/design/modules.md))。 ```ts // modules/audit.ts,在 cell.config.ts 里写 modules: ['./modules/audit'], audit: { url: '...' } import type { CellAPI } from '@celljs/kit'; export default function (cell: CellAPI) { const { url } = cell.options('audit', { url: 'http://localhost:9000' }); cell.addVitePlugin({ name: 'audit', transform: code => code }, { env: 'server' }); cell.on('build:done', event => { event.files['audit.json'] = JSON.stringify({ url, targets: event.project.targets }); }); } ``` - 加载顺序固定:依赖里的组件(package.json 的 `cell.toolchain`,按依赖顺序)→ `cell.config.ts` 的 `modules`(本地路径或包名,按书写顺序)。`{ source, toolchain: false }` 关掉某个组件的模块,`'!包名'` 连组件一起去掉。 - 工厂只做注册,不启动进程、端口、监听或定时器:`cell info` 也会执行工厂。 - `cell.config.ts` 里除 `preset`、`targets`、`modules`、`build`、`vite`、`hooks` 之外的顶层字段都是模块选项,必须由某个模块用 `options()` 或 `addConfigSchema()` 认领,否则报 `CELL_CONFIG_UNKNOWN_OPTION`(附拼写建议);`addConfigSchema` 接受 Standard Schema(zod、valibot 等),不合法报 `CELL_CONFIG_INVALID`。 - 工厂抛错报 `CELL_MODULE_ERROR`(带模块名);默认导出不是函数报 `CELL_MODULE_INVALID`。`cell dev` 中通知类事件的处理函数出错只报告不中断,`cell build` 中任何处理函数出错都失败。 - 每个模块注册了什么都列在 `cell info --json` 的 `toolchainModules` 里。 | API | 作用 | | --- | --- | | `command`、`mode`、`dev`、`ci`、`root`、`preset`、`components`、`logger` | 只读信息;`preset` 是命令行 / 配置 / 环境变量里写的名字,解析后的 preset 在 `config:resolved` 事件里 | | `options(key, defaults)`、`addConfigSchema(key, schema)` | 读并认领 `cell.config.ts` 的 `key` 字段(只能在工厂里同步调用) | | `updateConfig({ build: { external }, vite })` | 追加 external、在项目的 `vite` 之下合并 Vite 配置(项目优先);应用配置仍写在组件自己的 `cell.yml` | | `addVitePlugin(plugin, { env })` | 加 Vite 插件,`env` 为 `client` / `server` 时只作用于前端 / 后端 | | `addRuntimeModule(path, { target, static })`、`addVirtualModule(id, code)`、`setEntry(target, path)` | 追加 DI 模块、提供虚拟模块、指定入口 | | `addDevHandler(path, handler)`、`addWatchFile(path)` | `cell dev` 中在后端之前处理某个路径;文件变化时重启 | | `addPreset(preset)`、`addOutputFile(path, content)` | 注册 preset(参与 `--preset` 和 CI 识别);构建后写入输出目录的文件(`build:done` 的 `event.files` 可增删改) | | `addCheck(def)`、`addMcpTool(def)`、`addDocs(dir)` | 供 `cell check`、`cell mcp`、`cell docs` 使用 | | `on(event, handler)`、`events` | 订阅工具链事件(组合规则见 modules.md §3.2);模块之间的事件总线 | | `exec(cmd, args, opts)`、`resolve(spec)` | 不经 shell 执行命令;从项目根目录解析模块 | ## 错误码 所有失败都以 `CellError { code, message, file?, line?, hint, docs }` 输出,`code` 在 4.x 内保持稳定。 | 错误码 | 含义 | | --- | --- | | `CELL_ARGUMENT_INVALID` | 未知命令、选项或取值(退出码 2) | | `CELL_PROJECT_NOT_FOUND`、`CELL_PACKAGE_JSON_INVALID` | 根目录没有 package.json、某个 package.json 无法读取 | | `CELL_CONFIG_LOAD`、`CELL_CONFIG_AMBIGUOUS` | `cell.config.ts` 加载或执行失败、存在多个配置文件 | | `CELL_CONFIG_INVALID`、`CELL_CONFIG_CONFLICT` | 配置字段错误(带字段路径和拼写建议)、选项互相冲突 | | `CELL_CONFIG_PARSE`、`CELL_CONFIG_NOT_OBJECT`、`CELL_CONFIG_EXPRESSION` | `cell.yml` 不是合法 YAML、顶层不是映射、`${...}` 表达式错误 | | `CELL_CONFIG_INVALID`(cell.yml) | 没有归属的顶层键、组件没有的选项、类型不符;带文件、行号和拼写建议,自己的配置请放在 `app` 下 | | `CELL_CONFIG_KEY_CONFLICT`、`CELL_CONFIG_SCHEMA_INVALID` | 两个组件的 `cell.schema.json` 声明了同一个键、schema 用了不支持的关键字或项目 schema 声明了 `app` 以外的键 | | `CELL_COMPONENT_NOT_FOUND`、`CELL_LOCAL_MODULE_NOT_FOUND` | `modules` 里写的组件或本地模块不存在 | | `CELL_MODULE_NOT_FOUND`、`CELL_CORE_NOT_FOUND` | 组件声明的 DI 模块无法解析、项目没有依赖 `@celljs/core` | | `CELL_ROUTE_CONFLICT`、`CELL_ROUTE_INVALID` | `src/browser/pages` 里两个文件对应同一路径(`about.tsx` 与 `about/index.tsx`)、文件名不是合法的路径段 | | `CELL_CORE_EL_MISSING` | 项目的 `@celljs/core` 早于 4.1.0,没有构建期求值配置所需的 `@celljs/core/el` 入口 | | `CELL_PRESET_NOT_FOUND`、`CELL_PRESET_DUPLICATE`、`CELL_PRESET_CYCLE` | preset 不存在、重名、`extends` 成环 | | `CELL_BUILD_FAILED` | 打包失败,带出错文件和位置 | | `CELL_BUILD_CANCELLED`、`CELL_HOOK_FAILED` | hook 或模块取消了构建、hook 命令失败 | | `CELL_MODULE_ERROR`、`CELL_MODULE_INVALID` | toolchain 模块的工厂或处理函数出错、默认导出不是工厂函数 | | `CELL_CHECK_FAILED`、`CELL_DI_MODULE_FAILED` | 某项检查自身出错、`cell check` 加载后端 DI 模块失败 | | `CELL_SECURITY_*`、`CELL_SECURITY_CHECK_FAILED` | 生产环境不安全的设置(`cell check` 的 `security` 检查报告为 warning);生产启动时 `securityChecks: fail` 拒绝启动 | | `CELL_CONFIG_UNKNOWN_OPTION` | `cell.config.ts` 有没有模块认领的选项字段 | | `CELL_DEV_FAILED`、`CELL_DEV_BACKEND_FAILED`、`CELL_DEV_START_TIMEOUT` | `cell dev` 无法启动(端口等)、后端启动失败、后端入口超时未就绪 | | `CELL_DEV_BACKEND_NOT_RUNNING` | dev MCP 工具需要运行中的后端,但后端启动失败或尚未启动 | | `CELL_CONFIG_KEY_NOT_FOUND` | `cell config get` / `get_config` 的键不存在 | | `CELL_COMPILE_TOOL_MISSING`、`CELL_COMPILE_FAILED` | `--compile` 需要的 bun / deno / node 不存在、编译器报错(带输出的最后几行) | | `CELL_COMPILE_EXTERNALS`、`CELL_COMPILE_NO_SERVER` | 有留在 bundle 外的包、项目没有后端可编译 | | `CELL_TEMPLATE_NOT_FOUND`、`CELL_INIT_NOT_EMPTY` | `cell init` 的模板不存在、目标目录非空 | | `CELL_UNSUPPORTED` | 使用了尚未提供的功能 | | `CELL_OUTPUT_NOT_FOUND`、`CELL_PREVIEW_NO_SERVER` | `cell preview` / `cell deploy --skip-build` 前没有构建、产物里没有服务端 | | `CELL_DEPLOY_UNSUPPORTED`、`CELL_DEPLOY_OUTPUT_MISMATCH`、`CELL_DEPLOYER_NOT_FOUND` | preset 没有部署器、已有产物不是该 preset 构建的、部署器不存在 | | `CELL_DEPLOY_PLAN_FAILED`、`CELL_DEPLOY_FAILED`、`CELL_DEPLOY_CANCELLED` | 计划失败(如打包失败)、部署失败(凭据问题时 `hint` 说明如何设置)、hook 或模块取消了部署 | | `CELL_DEPLOY_CREDENTIALS_MISSING`、`CELL_DEPLOY_UNAUTHORIZED` | 没有云平台凭据、凭据无效或权限不足 | | `CELL_DEPLOY_API_ERROR`、`CELL_DEPLOY_NETWORK`、`CELL_DEPLOY_TIMEOUT`、`CELL_DEPLOY_PACKAGE_TOO_LARGE` | 云 API 报错(带云厂商错误码和请求 ID)、连不上云 API、等待函数就绪超时、代码包超过直传上限 | | `CELL_CLOUDFLARE_CONFIG_INVALID`、`CELL_CLOUDFLARE_CONFIG_UNSUPPORTED`、`CELL_CLOUDFLARE_COMPATIBILITY_DATE` | `wrangler.json(c)` 不是合法 JSON 或字段类型错误、使用了 `wrangler.toml`、`compatibility_date` 早于 `2026-03-17` | | `CELL_INTERNAL_ERROR` | Cell 或模块的缺陷;设置 `CELL_DEBUG=1` 可看到调用栈 | ## 测试:createTestApp ```ts import { createTestApp } from '@celljs/cli'; const app = await createTestApp({ root, mode: ['extra'] }); // mode 前面总有 test const res = await app.fetch('/users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }); const { UserService } = await app.import('/src/user-service.ts'); // 后端加载的那份模块 app.get(UserService); // DI 容器里的对象 await app.close(); // 停止应用(onStop) ``` `app.get(X)` 中测试从项目源码导入的类或 `Symbol('X')` 与应用里的不是同一个模块实例(应用在 Vite 的 module runner 里),按类名或 symbol 描述匹配;没有或重名时报 `CELL_DI_NO_BINDING` / `CELL_DI_AMBIGUOUS`。要拿到应用用的那一份(重名时、或要用模块里的其他导出),用 `app.import()`:项目内的文件写 `/src/...`,包写包名。带 `@Value` / `@Autowired` 的类必须从 `app.import()` 取:测试文件自己导入的这种类按名字匹配后解析会失败(`CELL_DI_NO_BINDING`)。 在测试进程里像 `cell dev` 一样从源码加载后端,不构建、不监听端口;请求经内存中的 req/res 对象走完整的 HTTP 处理链,`fetch` 返回标准 `Response`。启动失败抛 `CellError`(如 `CELL_DEV_BACKEND_FAILED`,带出错文件);后端没有 HTTP 入口时 `fetch` 报 `CELL_TEST_APP_NO_HTTP`。 ## 编程接口 ```ts import { CellCli } from '@celljs/cli'; const code = await new CellCli({ stdout, stderr, env }).run(['build', '--json']); ``` 也导出 `defineConfig`、`ConfigResolver`、`PresetRegistry`、`DeployerRegistry`、`EventBus`、`HookRunner` 等,用于测试和编写工具。 # Cell 此项目使用 Cell 4.8.0:具有依赖注入的 TypeScript、Node 的 `http` 服务器上的 MVC 控制器,以及 `cell` 工具链(Vite)。本指南与已安装的版本相匹配;升级后 `npx cell agent sync` 会重写它。 ## 布局 - `src/node/` 是后端,`src/browser/` 是前端,`src/common/` 是两端共享的代码。`src//module.ts` 是其端的 DI 模块:导入每个有组件的文件,以 `export default autoBind();` 结尾。它不导入的文件不会被加载。 - `cell.yml` 保存应用配置:顶层是组件的键(`server.port`),`app` 下是你自己的;只有标记为公开的键和 `app.public` 才会到达浏览器;错误的键会引发 `CELL_CONFIG_INVALID` 错误。`cell..yml` 按模式追加(`cell dev` 中是 `development`,`cell build` 中是 `production`,测试中是 `test`,用 `-m ` 可更多)。 - `cell.config.ts`(可选)仅配置工具链:`preset`、`modules`、`build`、`vite`、`hooks`。应用配置永远不在这里。 - package.json 有 `cell` 字段的依赖是组件:安装就够了,其 DI 模块和 `cell.yml` 自动加载。保持每个 `@celljs/*` 包在同一版本。 ## 编写代码 - 服务:在类上用 `@Component()`,在属性上用 `@Autowired(Type)` 注入。对于接口,导出一个同名的 `Symbol` 并用 `@Component(Symbol)` 绑定类。 - 配置:`@Value('app.key')` 读取 `cell.yml` 中 `app` 下的键,`@Value('server.port')` 读取组件的键。`${env.NAME ?? 'default'}` 在启动时求值并转换为键的架构类型。`CELL_SERVER__PORT=8080` 覆盖现有键。`npx cell config get --why` 告诉所有者以及前端是否看到它。 - HTTP:使用 `@celljs/mvc/lib/node` 中的 `@Controller('path')`、`@Get()`、`@Post()`、`@Put(':id')`、`@Delete(':id')`;参数 `@Param('id')`、`@Query('q')`、`@Body()`。返回的对象是 JSON;文本添加 `@Text()`。`String`、`Number`、`Boolean` 和 `Date` 参数被转换。要验证请求体,在依赖中添加 `@celljs/validation`(带 `class-validator`)并用有 class-validator 装饰器的类给 `@Body()` 参数类型化:无效体得到 400。 - API 文档:在依赖中添加 `@celljs/openapi`;应用在 `/openapi.json` 提供 OpenAPI 文档,在 `/api-docs` 提供 Swagger UI,从路由、参数、返回类型和 class-validator 装饰器构建。异步方法和数组需要 `@ApiResponse({ type: [Pet] })`;没有 class-validator 装饰器的属性需要 `@ApiProperty()`。 - gRPC:添加 `@celljs/grpc`;从 `.proto` 文本(`import source from './x.proto?raw'`)调用 `Proto.parse(source).service('pkg.Service')`,在实现其方法的类上用 `@GrpcService(Service)`(小驼峰,流是异步可迭代),调用一个用 `new GrpcChannel('host:50051').createClient(Service)`。抛出 `GrpcError(GrpcStatus.X, 'details')` 以获取状态。服务器监听 `grpc.server.port`(50051)。 - 错误:从 `@celljs/web/lib/node` 抛出 `HttpError(status, message)` 或 `NotFoundError(message)` 以获取状态;其他错误答 500。`@Catch(ErrorType)` 在控制器方法上处理该类型给控制器;一个 `@Component(ErrorHandler)` 带 `priority` 超过 2000 给整个应用。 - 中间件:`@Component(Middleware)` 实现 `handle(ctx, next)` 和 `priority`(`@celljs/web/lib/node`);`Context.getRequest()` 在任何地方获得当前请求。 - 启动和关闭工作:`@Component(ApplicationLifecycle)` 带 `onStart` / `onStop`,不是构造器。日志:`@Autowired(Logger)`。 - 支付:`@celljs/wechat-pay`(微信支付 API v3)、`@celljs/alipay`(支付宝网关)和 `@celljs/stripe`(带官方 `stripe` 包)注入客户端(`WechatPayClient`、`AlipayClient`、`StripeClient`),在 `backend.wechatPay` / `alipay` / `stripe` 下配置 `${env.X}` 秘密,并提供通知端点(`/wechat-pay/notify`、`/alipay/notify`、`/stripe/webhook`),验证签名并运行 `@WechatPayNotificationHandler('TRANSACTION.SUCCESS')`、`@AlipayNotificationHandler('TRADE_SUCCESS')` 或 `@StripeWebhookHandler('checkout.session.completed')` 组件。处理器必须是幂等的;抛出使提供商重试。微信金额是整数单位分,支付宝金额是字符串单位元。 - 前端和后端:在后端类上用 `@Rpc(Symbol)`,在前端组件中用 `useRpc(Symbol)`(`@celljs/react`),在前端 DI 组件中用 `@Autorpc(Symbol)`。当 `rpc.transport: websocket` 时,后端通过 `RpcContext.requireConnection().client(Symbol)`(`@celljs/rpc/lib/node`)调用前端的 `@RpcClient(Symbol)` 组件。 - 前端页面是 `src/browser/pages/` 的文件,组件作为默认导出(`users/[id].tsx` 是 `/users/:id`,`_layout.tsx` 包装目录);不要在任何地方导入它们。路由钩子和 `Link` 来自 `@celljs/react`。 - React 组件用 `useInject(Id)` 获得前端 DI 组件,用 `useValue(key)` 获得配置(来自 `@celljs/react`);没有全局容器。抛出的页面显示其布局内最近的 `src/browser/pages/_error.tsx`。测试在 `` 内渲染组件。 - 常见需求的包:数据库 `@celljs/typeorm`(+驱动;`autoBindEntities`、`OrmContext`、`@Transactional()`)、缓存 `@celljs/cache`、定时任务 `@celljs/schedule`、登录 `@celljs/security`、令牌 `@celljs/jwt`、出站 HTTP `@celljs/http`、LLM `@celljs/ai-core` + `@celljs/ai-pi`、支付(上述)。使用包前用 `npx cell docs ` 读其文档。 ## 验证 - `npx cell check --json` 检查类型、配置、装饰器设置和 DI 绑定。每个问题都有 `code`、`file`、`line` 和一个 `hint`。每次改动后运行。 - 测试用 `bun test` 运行;`@celljs/cli` 中的 `createTestApp()` 在测试过程中启动应用: ```ts const app = await createTestApp({ root: join(__dirname, '..') }); const res = await app.fetch('/todos', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }); const { TodoService } = await app.import('/src/node/todo-service.ts'); // 从 app.import() 获得类,不从测试导入 const service = app.get(TodoService); await app.close(); ``` - `npx cell dev` 运行应用并在保存时重载;`npx cell build` 构建到 `.cell/output`(`--single`:一个文件,`--compile`:一个可执行文件);`npx cell preview` 运行构建。 - `npx cell deploy --preset --dry-run` 构建并显示部署会改变什么,不需凭据。平台选项在 `cell.config.ts` 的预设名下(`fc: { region: 'cn-shanghai' }`);凭据来自环境变量。除非被问到,否则让真实 `cell deploy` 由用户完成。 ## 发现 - `npx cell info --json`:组件、DI 模块、入口、预设、钩子、输出。 - `npx cell config get --why`:合并的配置值及其设置文件和变量。 - `npx cell docs `:已安装 Cell 包和 Cell 指南的文档(配置、DI、HTTP、RPC、测试、部署),用已安装的版本。 - `npx cell mcp` 将这些作为 MCP 工具提供。当 `cell dev` 运行时,`http://127.0.0.1:/_cell/mcp` 有正在运行的应用的 `get_errors`、`list_routes`、`list_bindings`、`get_config`、`recent_requests` 和 `logs`。 - 错误读 `[CELL_CODE] message`,然后 `fix:` 和文档链接:做修复说的。 # 从 Cell 3.x 迁移到 4.0 本文面向已有的 Cell 3.x 项目,说明迁移到 4.0 要改什么。4.0 的工具链是重新设计的(设计见 [design/architecture.md](https://github.com/cellbang/cell/blob/main/docs/design/architecture.md)),**不兼容 3.x 的命令、hook、配置表达式和产物目录,也没有兼容层**,所以要按本文逐项修改。运行时 API(`@Component`、`@Autowired`、`@Value`、MVC、RPC、typeorm 等)不变,业务代码一般不用改。 ## 0. 4.0 包含的能力 | 能力 | 状态 | | --- | --- | | `cell dev`、`cell build`、`cell info`、`cell preview`、`cell.config.ts`、`hooks`、`--json` | 可用 | | `node` preset(Node.js HTTP 服务) | 可用,默认 | | `cell check` | 可用 | | `cell mcp`、`cell dev` 的 `/_cell/mcp` 端点、`cell config get`、`cell docs` | 可用 | | `cloudflare` preset 与 `cell deploy`(Cloudflare Workers) | 可用,见 [@celljs/cli 的 README](https://cell.cellbang.com/zh/cli#cloudflare) | | 云平台 preset 与 `cell deploy`(fc / scf / lambda / vercel) | 可用,见 [@celljs/cli 的 README](https://cell.cellbang.com/zh/cli#fcscflambda) | | `--single`、`--compile` 单文件与可执行文件 | 可用 | | `cell init` 与项目模板(`backend`、`fullstack`)、`cell agent sync` | 可用 | | 用环境变量覆盖任意配置项(`CELL_*`) | 可用 | | 进程内测试工具 `createTestApp()`(`@celljs/cli`) | 可用 | ## 1. 环境 - Node.js `^20.19.0 || >=22.12.0`(Vite 8 的要求)。构建出的服务端 bundle 可在 Node.js 18 及以上、Bun 1.2 及以上和 Deno 2 上运行(`fc`、`scf` 镜像里的 Node.js 较旧),运行时包的 `engines` 也是 `>=18.0.0`(基于 pi 的 `ai-pi`、`ai-agent`、`ai-coding-agent`、`tui` 为 `>=22.19.0`)。 - 包管理器不限。仓库自身用 Bun 开发。 ## 2. 依赖 ```jsonc { "dependencies": { "@celljs/core": "^4.0.0", "@celljs/express-adapter": "^4.0.0", // 新增:提供 HTTP 入口 "@celljs/mvc": "^4.0.0" }, "devDependencies": { "@celljs/cli": "^4.0.0" // 项目内安装,不再全局安装 } } ``` - **加上 `@celljs/express-adapter`。** 3.x 的 HTTP 入口由运行时插件隐式带进来;4.0 由 preset 按名字(`node` preset 用 `http`)从组件声明的 `cell.entries` 里选入口,这个入口由 `@celljs/express-adapter` 提供。缺少时 `cell build` 会给出警告,产物启动后不监听端口。 - **删除** `@celljs/cli-service`、`@celljs/cli-common`、`@celljs/cli-runtime`、`@celljs/dev-server`、`@celljs/frameworks`、`@celljs/testing` 和 `@celljs/*-plugin`,以及只为 webpack 服务的依赖(各种 `*-loader`、`mini-css-extract-plugin`、babel 等)。 - 用 Sass / Less / Stylus 时自己安装预处理器(如 `sass`),Vite 检测到就会使用。 - 带安装脚本的原生依赖(`better-sqlite3` 等)要确保包管理器执行了它的安装脚本(Bun 需要列进 `trustedDependencies`),否则构建能过、运行时报找不到 `.node` 文件。 ## 3. 命令 | 3.x | 4.0 | | --- | --- | | `cell serve` | `cell dev`:前后端同一个端口,前端 HMR,后端改文件自动重载,`cell.yml` / `cell.config.ts` / `package.json` 变化时自动重启 | | `cell build -m prod` | `cell build -m prod` | | 运行 `.cell/dist/backend/index.js` | `cell preview`,或直接 `node .cell/output/server/index.mjs` | | `cell deploy -m fc` | `cell deploy --preset fc`(以及 `scf`、`lambda`、`vercel`、`cloudflare`;`--dry-run` 只给出计划)。3.x 的 `cell.fc.yml` 等平台配置改为 `cell.config.ts` 的 `fc: { region, memory, ... }`,凭据从环境变量读取,不再读 `~/.cell` 或 profile 文件 | | `cell init` | `cell init