# 从 Cell 3.x 迁移到 4.0

本文面向已有的 Cell 3.x 项目，说明迁移到 4.0 要改什么。4.0 的工具链是重新设计的（设计见 [design/architecture.md](./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](../dev-packages/cli/README.md#cloudflare) |
| 云平台 preset 与 `cell deploy`（fc / scf / lambda / vercel） | 可用，见 [@celljs/cli 的 README](../dev-packages/cli/README.md#fcscflambda) |
| `--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. 依赖

```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 <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](../dev-packages/cli/README.md)。

## 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](../examples/docker-app/Dockerfile)）。端口取 `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}` 插值，也能用环境变量改任意已有的配置项：

```bash
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`

```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](./design/modules.md)）。本地模块写在 `cell.config.ts` 的 `modules` 里（TypeScript 不用预编译），能力清单见 [@celljs/cli 的 README](../dev-packages/cli/README.md#toolchain-模块)。

例如 prisma 项目（见 [examples/prisma-app](../examples/prisma-app/cell.config.ts)）：

```ts
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`）。

```ts
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](../examples/standalone-app/test/app.service.spec.ts)。
- `@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](../examples/backend-app/test/home-controller.spec.ts)。

## 8. 组件作者

组件的全部声明都在 package.json 的 `cell` 字段里，`cell.yml` 只放默认的应用配置：

```jsonc
{
  "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. 迁移后的检查清单

1. `cell info` 的 `warnings` 为空（部署环境里的 `CELL_BACKEND__*` 变量也可以带上一起检查），`targets`、`components` 与预期一致（例如 mode 拼错会在这里提示）。
2. `cell build --json` 输出 `"ok": true`，`warnings` 为空，`entry` 是 `@celljs/express-adapter/lib/node/http-application-entry`（或你选的入口）。
3. `cell preview` 后接口和页面正常。
4. 把 `.cell/output` 拷到项目外的空目录，用 `node server/index.mjs` 运行，确认不依赖项目的 `node_modules`。
5. 用到原生模块或运行时读文件的库（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`](../ai-packages/ai-pi/README.md)（模型名 `pi:<provider>/<model>`）；前端用 `@celljs/react`。
- **`@celljs/security` 不再内置用户。** 原来依赖内置 `admin` 账号的应用（这个账号的哈希从 2022 年起实际就已无法登录）要自己配置：在 `cell.yml` 里写 `cell.security.username` / `password`（`PasswordEncoder` 生成的哈希），或者绑定自己的 `UserService`，见 [@celljs/security](../packages/security/README.md#users)。没有用户来源时启动会打警告。
- **密码哈希升级不可回滚。** 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`，并给出安装命令。
