从 Cell 3.x 迁移到 4.0
本文面向已有的 Cell 3.x 项目,说明迁移到 4.0 要改什么。4.0 的工具链是重新设计的(设计见 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 |
云平台 preset 与 cell deploy(fc / scf / lambda / vercel) | 可用,见 @celljs/cli 的 README |
--single、--compile 单文件与可执行文件 | 可用 |
cell init 与项目模板(backend、fullstack)、cell agent sync | 可用 |
用环境变量覆盖任意配置项(CELL_BACKEND__* / CELL_FRONTEND__*) | 可用 |
进程内测试工具 createTestApp()(@celljs/cli) | 可用 |
1. 环境
- Node.js
^20.19.0 || >=22.12.0(Vite 8 的要求)。构建出的服务端 bundle 可在 Node.js 18 及以上运行(fc、scf镜像里的 Node.js 较旧)。 - 包管理器不限。仓库自身用 Bun 开发。
2. 依赖
{
"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 按名字(nodepreset 用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 <template> <dir>:新模板 backend、fullstack(React + RPC),自带 AGENTS.md、.mcp.json;3.x 的模板不再提供 |
cell props / cell config / cell update / cell runtime | 删除;查看项目和配置用 cell info(--app-config 输出合并后的 cell.yml),单个配置项及其来源用新的 cell config get <key> --why |
所有命令支持 --json:stdout 只输出一行带 schemaVersion 的结果,失败时退出码为 1(用法错误为 2)。完整说明见 @celljs/cli 的 README。
4. 产物
| 3.x | 4.0 |
|---|---|
.cell/dist/backend/index.js(webpack,CommonJS) | .cell/output/server/index.mjs:一个文件,依赖全部内联(--format cjs 时为 index.cjs) |
.cell/dist/frontend/ | .cell/output/public/ |
部署前再 npm install 外部依赖 | 只有原生模块、build.external 和组件声明的可选依赖拷到 server/node_modules |
| — | .cell/output/manifest.json:preset、格式、入口,供 cell preview 和部署器读取 |
| — | .cell/generated/:DI 装配代码、合并后的配置、项目模型,便于排查 |
.cell/output 可以整个拷到别处运行,不依赖项目的 node_modules(Dockerfile 见 examples/docker-app)。端口取 cell.server.port(默认 3000),监听地址取 cell.server.host(默认 127.0.0.1,容器里要设成 0.0.0.0),都可以用环境变量覆盖(见 5.3),cell preview --port 也可以改端口。3.x 的 SERVER__PORT / SERVER__HOST 不再生效,改为 CELL_BACKEND__SERVER__PORT / CELL_BACKEND__SERVER__HOST。
把 .cell 加进 .gitignore。
5. 配置文件
4.0 把配置分成两类,职责不重叠:
| 文件 | 内容 |
|---|---|
cell.yml、cell.<mode>.yml | 应用配置,即 @Value 读取的内容 |
cell.config.ts(可选) | 工具链配置:preset、targets、modules、build、vite、hooks |
5.1 cell.yml 里要删掉的内容
| 3.x 写法 | 4.0 |
|---|---|
mode: [database] | 删除。mode 只来自命令行:cell build -m database,写进 package.json 的 build 脚本 |
targets: [backend]、entry: ... | 删除。目标默认是 backend,加上组件声明的目标(装了 @celljs/react 就会经 @celljs/widget 构建前端);要改就在 cell.config.ts 写 targets |
webpack:、devServer:、webpackHooks、serveHooks | 删除。改 Vite 配置用 cell.config.ts 的 vite 字段 |
cell.webpack.filterWarningsPlugin(可选依赖) | 组件在 package.json 的 cell.optionalDependencies 里声明,解析不到时自动 external |
cell-test.yml 这类 mode 文件 | 只识别 cell.<mode>.yml(如 cell.test.yml),cell-<mode>.yml 要改名 |
cell build 自动带上 production mode,cell dev 带上 development,开发/生产差异写在 cell.production.yml / cell.development.yml。合并顺序:组件的 cell.yml(按依赖顺序)→ 项目 cell.yml → 各 mode 文件(按 -m 顺序)。对象深合并,数组整体替换。frontend: / backend: 分区保留,含义不变。
5.2 表达式
- 只保留
${...}插值,上下文是{ ...配置, env }:"${env.DB_HOST ?: 'localhost'}"、"${cell.server.path}"。 - 后端在运行时求值:同一个产物可以部署到多个环境,密钥不会写进产物。前端在构建时求值。
- 删除了
onTarget、cliContext、currentMode等只在构建时存在的上下文。按目标区分写在frontend/backend分区,按环境区分写在 mode 文件。 - 含
?:的 YAML 值必须加引号。 - 密钥只能放在
backend:分区下。 顶层公共键和frontend:分区的值会原样打进前端产物,浏览器能下载到;前端配置里写${env.X},这个环境变量的值也会进前端产物。 - 表达式的结果不会再被求值(4.1.0 起):环境变量的值里即使带
${...}也按原文使用,前端构建时同样如此。配置之间的引用照常解析,顺序不限;需要再求值时显式写${env.X | eval}。
5.3 用环境变量覆盖配置项
不改 cell.yml、不加 ${env.X} 插值,也能用环境变量改任意已有的配置项:
CELL_BACKEND__SERVER__PORT=8080 node server/index.mjs # cell.server.port
CELL_BACKEND__SERVER__HOST=0.0.0.0 # cell.server.host
CELL_BACKEND__TYPEORM__ORM_CONFIG__0__HOST=db.internal # cell.typeorm.ormConfig[0].host
CELL_FRONTEND__API_BASE=https://api.example.com cell build # 前端的 cell.apiBase,构建时生效前缀决定目标:
CELL_BACKEND__由服务端在启动时应用(同一个产物在不同环境用不同的值),CELL_FRONTEND__在cell build时应用到前端配置。其余部分用
__分隔层级。第一段先在顶层找,找不到再到cell下找:顶层通常没有server,所以CELL_BACKEND__SERVER__PORT就是cell.server.port;顶层键(如app)直接写CELL_BACKEND__APP__TITLE。CELL_BACKEND__CELL__…总是指cell下的键,项目顶层有与之同名的键时用它(cell info会提示)。匹配时不区分大小写,也忽略_和-(ORM_CONFIG对应ormConfig,SERVE_STATIC对应serve-static),数组元素用下标。只能覆盖已经存在的键(组件或项目的
cell*.yml里有的)。新键先在cell.yml里写上默认值。值按
cell.yml里原值的类型转换:原值 接受的写法 数字 JSON 写法的有限数: 8080、-1、0.5、1e3。Infinity、0x10、+1、08、空串、首尾空格、超过 2^53 的整数都报错布尔 true/false,不区分大小写;1、yes报错对象、数组 JSON,且种类要一致(数组只能换成数组) null是 JSON 就按 JSON( 5、true、null、{...}),否则当作文本字符串 原样使用 值如果整体是一个
${...}表达式,不论原值是什么类型都直接保留,和写在cell.yml里一样参与求值,类型由求值结果决定。例如平台通过PORT给端口时写CELL_BACKEND__SERVER__PORT='${env.PORT}'。表达式外还有其他文字(80${env.X})时按上表处理。原值本身是
${...}表达式时,它在cell.yml里是字符串,覆盖值也就按文本处理:false会是字符串"false"。要覆盖成布尔值或数字,写成表达式:'${false}'、'${5432}'。@celljs/typeorm的默认数据库配置读取DB_TYPE、DB_HOST、DB_PORT、DB_SYNC(只有true才开启表结构同步)、DB_DATABASE,并转换成正确的类型。项目自己在cell.yml里写了ormConfig时用CELL_BACKEND__TYPEORM__ORM_CONFIG__0__*覆盖。数组元素用规范下标(
0、1、12),00、01视为不存在的键。一个键只能由一个变量设置:两个变量指向同一个键(
SERVER__PORT和CELL__SERVER__PORT),或一个指向某个键、另一个指向它里面的键(整体替换…ORM_CONFIG又改…ORM_CONFIG__0__HOST),这些变量都不生效并报出来,结果与变量顺序无关。要替换整个对象或数组又改其中一项,直接写进 JSON。后端还有
paths.public:前端产物目录,默认public,相对.cell/output(服务端启动时解析为绝对路径;serve-static 的root: ${paths.public}引用它)。可以在cell.yml里写,也可以用CELL_BACKEND__PATHS__PUBLIC=/srv/www覆盖。覆盖发生在
${...}求值之前,所以endpoint: http://localhost:${cell.server.port}会用覆盖后的端口;值里的${...}也会像写在cell.yml里一样求值。找不到键或类型不符的变量不会生效:服务端启动时在 stderr 打印
[cell] CELL_BACKEND__… is ignored: …,cell build对前端变量给出警告,cell info两种都检查,并在envOverrides里列出每个变量对应的配置路径(不输出值)。
5.4 cell.config.ts
import { defineConfig } from '@celljs/cli';
export default defineConfig({
preset: 'node', // 默认 node;也可 --preset、CELL_PRESET
targets: ['frontend', 'backend'], // 一般不用写
modules: ['!@celljs/serve-static'], // 去掉某个组件
build: { format: 'esm', external: ['sharp'], sourcemap: true },
vite: { resolve: { alias: { '~': '/src' } } }, // 合并进 Vite 配置
hooks: { 'build:before': 'prisma generate' },
});配置在加载后逐项校验,写错字段报 CELL_CONFIG_INVALID,并给出字段路径和"你是不是要写"。应用配置不能写进 cell.config.ts。
6. hook
3.x 的约定文件 hook(src/hooks/webpack.ts、serve.ts、deploy.ts、build.ts 等)全部失效:
- 构建前后要跑命令:写在
cell.config.ts的hooks里。命令在项目根目录执行,PATH 里有项目的node_modules/.bin,环境变量带CELL_EVENT、CELL_PRESET、CELL_MODE、CELL_OUT_DIR。build:before里命令失败会取消构建。可订阅的事件:modules:done、config:resolved、build:before、output:done、close,cell dev还有dev:ready、dev:reload,cell deploy有deploy:before、deploy:plan、deploy:done。 - 改 webpack 配置:改为
cell.config.ts的vite字段,或写 Vite 插件。 - 需要代码逻辑的扩展(改产物、注册 preset 等):写 toolchain 模块(工厂函数,见 design/modules.md)。本地模块写在
cell.config.ts的modules里(TypeScript 不用预编译),能力清单见 @celljs/cli 的 README。
例如 prisma 项目(见 examples/prisma-app):
hooks: {
'build:before': 'prisma generate',
// 内联后的 prisma client 在 server/index.mjs 旁边查找 schema 和查询引擎
'output:done': 'cp prisma-client/schema.prisma prisma-client/libquery_engine-* "$CELL_OUT_DIR/server/"',
}7. 测试
@celljs/testing(Test.createHttpServer()、Test.createContainer())已删除,改用 @celljs/cli 的 createTestApp():在测试进程里从源码启动后端(DI 容器和 HTTP 处理链),不构建、不监听端口,cell.config.ts、toolchain 模块和 preset 的入口与 cell dev 一致,mode 为 test(读 cell.test.yml)。
import { afterAll, beforeAll, expect, it } from 'bun:test';
import { createTestApp, type TestApp } from '@celljs/cli';
let app: TestApp;
beforeAll(async () => (app = await createTestApp())); // 项目根目录默认取当前目录,也可传 root
afterAll(() => app.close());
it('lists users', async () => {
const res = await app.fetch('/users'); // 标准 Response;init 同 fetch
expect(await res.json()).toEqual([]);
});app.fetch(path, init)支持方法、头、请求体,响应保留状态码、重定向和多个Set-Cookie(不跟随重定向)。app.container/app.get(id)取 DI 容器里的对象。- 只测 DI 和服务、不需要 HTTP 时,也可以用
ApplicationFactory.create({}, AppModule),见 examples/standalone-app/test。 @celljs/cli只有 ES 模块。CommonJS 项目(package.json 没有"type": "module")在 TypeScript 里导入它,要把 tsconfig 的module/moduleResolution设为nodenext(TypeScript ≥ 5.8,按 Node 22 的require(esm)处理),否则报 TS1479。- 原来用 supertest 起服务的测试改成
app.fetch,见 examples/backend-app/test。
8. 组件作者
组件的全部声明都在 package.json 的 cell 字段里,cell.yml 只放默认的应用配置:
{
"cell": {
"modules": { "backend": ["lib/node/module"] }, // DI 运行时模块(frontend / backend)
"staticModules": { "frontend": ["lib/browser/static-module"] },
"targets": ["frontend"], // 装上本组件就要构建的目标(只写额外需要的)
"entries": { "http": "lib/node/http-application-entry" }, // 具名后端入口,由 preset 按名字选用
"optionalDependencies": ["pg-native"], // 可能没装的依赖,解析不到时 external
"toolchain": "lib/cell-module" // toolchain 模块
}
}- 删除组件里的
src/hooks、cell.remote.yml,以及cell.yml里的targets、entry、webpack。 - 框架包以 ES 模块构建,打包时按
exports的import条件解析到esm/。
9. 迁移后的检查清单
cell info的warnings为空(部署环境里的CELL_BACKEND__*变量也可以带上一起检查),targets、components与预期一致(例如 mode 拼错会在这里提示)。cell build --json输出"ok": true,warnings为空,entry是@celljs/express-adapter/lib/node/http-application-entry(或你选的入口)。cell preview后接口和页面正常。- 把
.cell/output拷到项目外的空目录,用node server/index.mjs运行,确认不依赖项目的node_modules。 - 用到原生模块或运行时读文件的库(prisma、sqlite 驱动等)时,确认需要的文件在
server/或server/node_modules里。
10. 4.0.0 之后的变化(尚未发布,main 上已合入)
这些改动不提供兼容层,升级时逐项检查:
- 删除的包:
@celljs/config、@celljs/rpc-core、@celljs/ai-openai、@celljs/ai-anthropic、@celljs/ai-ollama、@celljs/ai-deepseek、@celljs/shell、@celljs/grommet、@celljs/vue。模型统一用@celljs/ai-pi(模型名pi:<provider>/<model>);前端用@celljs/react。 @celljs/security不再内置用户。 原来依赖内置admin账号的应用(这个账号的哈希从 2022 年起实际就已无法登录)要自己配置:在cell.yml里写cell.security.username/password(PasswordEncoder生成的哈希),或者绑定自己的UserService,见 @celljs/security。没有用户来源时启动会打警告。- 密码哈希升级不可回滚。 4.0.x 存下的哈希仍能校验;
UserService实现了updatePassword()时,用户登录成功后哈希会换成新格式(pbkdf2-sha256$…)。换过之后再回退到 4.0.x,这个用户就登录不了了。 - HTTP Basic 现在能正常认证(4.0.x 中所有密码都校验失败),默认仍关闭。它每个请求都算一次 PBKDF2(约 45 ms),只适合低频的机器对机器调用。
- OpenID Connect:ID Token 必须带回授权请求里的 nonce,否则以
invalid_nonce拒绝;同一个授权响应只接受一次。 - 访问日志默认不输出:每个请求的
starting/ending日志改为 debug 级别。要看就设cell.logger.level: debug(或CELL_BACKEND__CELL__LOGGER__LEVEL=debug)。 mysql2需要自己安装:它改为@celljs/typeorm的可选 peer 依赖,用哪个数据库就装哪个驱动;缺少时启动报CELL_TYPEORM_DRIVER_MISSING,并给出安装命令。