工程规范、测试与发布
5.1 核心原则
- 纯函数优先: 所有工具函数必须是纯函数,相同输入产生相同输出,无副作用
- 不可变性: 使用
const、readonly、Object.freeze()、展开运算符而非突变 - 函数组合: 使用 pipe/compose 模式组合函数,避免深层嵌套
- 高阶函数: 使用高阶函数抽象通用模式
- 类型安全: 严格 TypeScript,避免
any,充分利用泛型 - 无类设计: 优先使用工厂函数和闭包而非 class;必要时仅在运行时核心使用 class (如 Router、DevServer)
- 核心纯函数: 路由解析、配置归并、代码生成与类型计算保持纯函数;Hono app、Hookable、文件系统、网络和可变上下文位于明确的 adapter/effect 边界,并通过参数注入依赖
5.2 文件组织规范
// 1. 类型导入放最前
import type { Config, ResolvedConfig } from '../types';
import type { Preset } from '../preset/types';
// 2. 外部依赖
import { resolve, join } from 'pathe';
import { defu } from 'defu';
import { getLogger } from '@ubean/shared/logger';
// 3. 内部依赖
import { readConfig } from './loader';
import { resolvePaths } from './resolvers/paths';
// 4. 常量定义 (纯数据)
const DEFAULT_CONFIG = {
srcDir: './',
output: {
dir: './.output'
}
} as const;
// 5. 纯工具函数 (不依赖外部状态)
// - 命名: 动词开头,小写驼峰
// - 必须有 JSDoc 注释说明用途、参数、返回值
// - 必须有类型标注
/**
* 合并用户配置与默认配置
* @param userConfig - 用户配置
* @param defaults - 默认配置
* @returns 合并后的配置
*/
function mergeConfig<T extends Record<string, unknown>>(userConfig: Partial<T>, defaults: T): T {
return defu(userConfig, defaults) as T;
}
// 6. 主要导出函数
// - 命名: 具名导出优先
// - 复杂函数内部拆分为小的纯函数
/**
* 加载并解析 ubean 配置
* @param rootDir - 项目根目录
* @param opts - 加载选项
* @returns 解析后的配置
*/
export async function loadOptions(rootDir: string, opts: LoadConfigOptions = {}): Promise<ResolvedConfig> {
const rawConfig = await readConfig(rootDir, opts);
const preset = await resolvePreset(rawConfig.preset, { dev: opts.dev });
const withDefaults = mergeConfig(rawConfig, DEFAULT_CONFIG);
return resolvePaths(withDefaults, rootDir);
}
// 7. 避免: 默认导出、class、let 突变、any 类型5.3 异步函数规范
// ✅ 好的做法: 返回 Promise,使用 async/await
async function readJsonFile<T>(path: string): Promise<T> {
const content = await fsp.readFile(path, 'utf-8');
return JSON.parse(content) as T;
}
// ✅ 好的做法: 错误处理返回 Result 类型或抛出特定错误
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
async function tryReadJson<T>(path: string): Promise<Result<T>> {
try {
const value = await readJsonFile<T>(path);
return { ok: true, value };
} catch (error) {
return { ok: false, error: error as Error };
}
}5.4 类型设计规范
// ✅ 使用 interface 定义对象形状,type 定义联合类型/工具类型
export interface UbeanOptions {
readonly rootDir: string;
readonly preset: PresetName;
readonly dev: boolean;
}
// ✅ 使用字面量类型 + as const
export const PRESET_NAMES = ['node-server', 'bun', 'deno', 'cloudflare', 'vercel'] as const;
export type PresetName = (typeof PRESET_NAMES)[number];
// ✅ 使用泛型保持类型安全
function createHandler<T extends EventHandler>(handler: T): T {
return handler;
}
// ✅ 条件类型做类型推断
type InferLoaderData<T> = T extends () => Promise<{ data: infer D }> ? D : never;6. 测试策略
6.1 测试框架
- vitest (vite-plus 集成版本)
- @vitest/coverage-v8 (覆盖率)
6.2 测试类型
- 单元测试 (
test/unit/)- 纯函数测试: 工具函数、配置解析、路由匹配
- 不依赖文件系统或网络
- 快速执行,覆盖率目标 > 90%
- 集成测试 (
test/integration/)- 构建流程测试
- 开发服务器测试
- Preset 适配测试
- 使用 test/fixtures 中的完整项目
- 浏览器端到端测试 (
test/e2e/)
- 使用 Playwright 验证 SSR hydration、客户端导航、表单 action 与错误页
- 只对正式支持的 preset 执行,不以模拟器替代正式平台 smoke test
- 类型测试
- 使用
expectTypeOf验证类型推导
- 使用
- 打包与部署 smoke test
pnpm pack后在独立 fixture 安装并验证公开 exports、CLI 与类型声明- 对每个正式 preset 执行
dev、build、preview和目标平台部署 smoke test
6.3 持续验收门槛
测试不是最后阶段的收尾任务。每个实现阶段都必须新增或更新对应 fixture,并在合并前满足以下门槛:
- 单元测试覆盖新增的纯计算、扫描和代码生成逻辑。
- 至少一个真实 fixture 覆盖新增能力的
dev、build和preview路径。 - 公开 TypeScript API 添加正反类型测试;生成文件变更需验证增量更新与冷启动结果一致。
- 改动 SSR、路由或页面协议时添加浏览器端到端测试。
- 改动 preset 能力时更新能力矩阵,并运行该 preset 的本地或远程 smoke test。
- 改动客户端传输时,覆盖 ofetch 默认路径、XHR
FormData上传进度、取消、超时、未知 total、HTTP/网络错误归一化,以及 SSR/edge 的不支持诊断。
覆盖率用于发现盲区,不作为替代契约测试的发布标准。核心运行时与公开 API 默认纳入统计;仅生成代码、平台不可执行的 shim 和经批准的适配器分支可排除,并在配置旁说明原因。
6.4 当前验证基线(2026-09-10)
- 主包(
ubean)自身无测试套件 —— 测试位于其所验证的子包中。 - 核心子包:
@ubean/server382、@ubean/builder234、@ubean/islands205(directive / paired-components / server-client-components / islands-registry / server-component-rerender)、@ubean/vue202、@ubean/routes152(含 Server Actions)、@ubean/client130、@ubean/cli96、@ubean/config95、@ubean/devtools41、@ubean/preset88。 - 扩展包:
@ubean/icon32、@ubean/auth15、@ubean/image45、@ubean/content93(含 30 个全文搜索测试)、@ubean/integrations39(pwa / fonts)、@ubean/seo116、@ubean/markdown21、@ubean/i18n22、@ubean/pages63、@ubean/scan13、@ubean/shared54、@ubean/ai15、@ubean/app69。 - 示例:
ubean-test783(含 prerender fixture)、client-only-spa30。 - 全仓库合计 3035 个测试通过。
pnpm typecheck:通过。编译器版本由pnpm-workspace.yaml中的 workspace overridetypescript: '6.0.3'固定,保证所有子包解析到同一版 TypeScript。pnpm build:通过(主包 + 全部 8 个扩展包,含@ubean/devtools与@ubean/islands)。- 路线图中标为 ✅ 的任务必须有对应源码、公开调用路径和与风险相称的验证;命令骨架、正则提取或未接通的运行时路径不得作为完整交付标记。
6.5 测试配置
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { resolve } from 'pathe';
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
exclude: ['test/fixtures/**', 'node_modules/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.ts'],
exclude: ['src/**/*.d.ts', 'src/**/__generated__/**']
},
testTimeout: 30000,
hookTimeout: 30000
},
resolve: {
alias: {
ubean: resolve(__dirname, 'src/index.ts'),
'ubean/server': resolve(__dirname, 'src/server.ts')
}
}
});6.6 测试示例
// test/unit/routing.test.ts
import { describe, it, expect } from 'vitest';
import { parseRoutePattern, matchRoute } from '../../src/utils/route';
describe('route utils', () => {
describe('parseRoutePattern', () => {
it('should parse static routes', () => {
const result = parseRoutePattern('/users');
expect(result).toEqual({
pattern: '/users',
params: [],
wildcard: false
});
});
it('should parse dynamic params', () => {
const result = parseRoutePattern('/users/:id');
expect(result.params).toEqual(['id']);
});
it('should parse catch-all routes', () => {
const result = parseRoutePattern('/blog/**');
expect(result.wildcard).toBe(true);
});
});
describe('matchRoute', () => {
it('should match static routes', () => {
const match = matchRoute('/users', '/users');
expect(match).not.toBeNull();
expect(match?.params).toEqual({});
});
it('should extract dynamic params', () => {
const match = matchRoute('/users/:id', '/users/123');
expect(match?.params).toEqual({ id: '123' });
});
});
});7. CLI 命令设计
CLI 命令清单与框架实现详见 运行时与开发体验 §4.13。
8. 导出设计
8.1 聚合器子路径契约(packages/ubean/package.json)
发布的 ubean 包是聚合器:主入口 re-export 所有 @ubean/* 子包,每个子路径切出一个能力域并带有明确的环境边界。真实的 exports 映射(构建产物为 .d.ts + .js):
{
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
"./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" },
"./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
"./ssr": { "types": "./dist/ssr.d.ts", "import": "./dist/ssr.js" },
"./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
"./build": { "types": "./dist/build.d.ts", "import": "./dist/build.js" },
"./i18n": { "types": "./dist/i18n.d.ts", "import": "./dist/i18n.js" },
"./scaffold": { "types": "./dist/scaffold.d.ts", "import": "./dist/scaffold.js" }
},
"bin": {
"ubean": "./bin/ubean.mjs"
}
}不存在 ./handler、./openapi、./client-xhr、./internal、./cron、./response、./env、./_env、./pages*、./devtools、./vue*、./database、./storage、./kv、./cache、./sse、./ws、./task、./config、./builder、./types、./routes 等子路径——这些能力位于各 @ubean/* 子包中。
| 子路径 | 聚合自 | 能力域边界 | 典型消费者 |
|---|---|---|---|
ubean | @ubean/shared / seo / pages / markdown + islands runtime + logger + 内联 defineConfig | Isomorphic:契约上浏览器安全——不含 node:*、文件系统扫描、Hono 服务端运行时 | 同构代码;客户端自动导入的基础 |
ubean/server | @ubean/app + @ubean/routes + @ubean/server + @ubean/shared/node + hono-openapi + logger/hono | 仅服务端:Hono 应用工厂(createUbeanApp)、defineHandler/defineAction、ISR/route-rules/OpenAPI、cache/db/queue/cron/ws/sse、validator/describeRoute;含 Hono 与 node:* | API 路由、src/middleware/、src/server.ts |
ubean/build | @ubean/build/prerender + @ubean/preset + @ubean/config + @ubean/build/codegen + @ubean/scan + Vite 插件本体 | 构建时:SSG 预渲染、preset(definePreset/detectPreset)、配置加载、codegen 预设、扫描器;含 node:* 与 oxc WASM | 构建脚本、vite.config.ts、CI |
ubean/client | @ubean/client + @ubean/routes/runtime + islands 注册表桥接 | 框架客户端:createUbeanClientApp、router/head/i18n 运行时、callAction/useAction/useFormAction/invokeServerFn、createServerHead、hydrateIslands | 虚拟模块、客户端入口 |
ubean/i18n | @ubean/i18n + @ubean/i18n/routing | 服务端 i18n:ALS t()/d()/n()、区域路径编译、检测中间件;含 node:async_hooks | 构建时 i18n、Hono 中间件 |
ubean/ssr | @ubean/client/ssr | SSR 渲染器:createVueRenderer | 自定义 SSR 入口 |
ubean/vite | @ubean/build(core + vue)+ @ubean/islands + server actions 插件 | Vite 插件组合:单一 ubeanPlugin() 入口 | vite.config.ts |
ubean/scaffold | @ubean/cli 脚手架层 | 脚手架:scaffold/deleteScaffold/recoverScaffold/listScaffoldableFiles + 机器可读清单(getScaffoldManifest) | studio / IDE 插件 |
该契约强制执行的规则:
- 浏览器代码从 isomorphic 主入口或
ubean/client导入;任何涉及 Hono /node:*的能力必须来自ubean/server、ubean/build或ubean/i18n。 bin指向./bin/ubean.mjs——转发启动器,导入@ubean/cli/cli(CLI 实现位于@ubean/cli)。- 新增或移除子路径时必须同步更新本契约与引用它的文档页面。
8.2 发布范围与平台能力契约
为避免“声明支持”与实际运行时语义不一致,版本支持分为正式支持、实验性和社区/按需三档。只有通过对应部署 smoke test 的 preset 才能进入正式支持列表。
| 版本 | 正式支持 | 实验性 | 不在承诺范围 |
|---|---|---|---|
| v0.1 | Node.js (node-server) | Cloudflare Workers(完成单独验收后) | Bun、Deno、Vercel、Netlify 及其他平台 |
| v0.2+ | 由能力矩阵与 CI 结果决定 | 新增 preset 先以实验性发布 | 未通过矩阵验收的平台 |
每个 preset 必须显式声明其 capabilities(19 个真实能力键:staticServe、websocket、sse、cronTriggers、queues、kv、storage、database、envVars、secrets、nodeCompat、streaming、compression、https、http2、middleware、bodyLimit、multipart、rpc)。构建器根据已启用功能和 preset 能力做预检查:
- 能力缺失时在构建期给出功能、配置位置、目标 preset 与替代方案,不静默降级。
- cron 仅在具备平台 trigger 或长生命周期进程能力时启用;serverless preset 不提供“内置常驻调度器”。
- ISR、WebSocket、Queue、文件存储等功能必须在每个 preset 中声明其一致语义、限制和测试环境;不能保证一致语义时应作为 preset 扩展而非核心能力。
8.3 客户端与公开 API 边界
ubean 不自研浏览器 HTTP 客户端。直接 HTTP 调用使用标准 fetch 或注入的 @soybeanjs/fetch(createRequest / toFlatRequest;@soybeanjs/fetch/openapi 提供 createTypedClient / toFlatTypedClient);数据层(useData / useFetch)经 setDefaultFetch 注入。internalFetch 是进程内分发器(直调框架 handler、无网络跳),不是 fetch 中间件栈。
- 服务端框架代码(API 路由、loader)不得依赖打包进框架的 HTTP client —— 使用
fetch/internalFetch/ 注入的@soybeanjs/fetch实例。 - OpenAPI 类型仅用于编译期参数与响应推导,运行时不加载 OpenAPI 文档。
- 发布
exports映射即 §8.1 的聚合器子路径契约 —— 不存在./experimental/*、./internal或./_env入口(这些是历史单包遗留)。 - 每次发布前使用
pnpm pack安装到独立 fixture,验证所有公开入口、条件导出和类型声明。
9. 实现规范与参考资源
9.1 UI 组件规范 (DevTools)
DevTools 面板的 UI 实现必须使用 @vean/ui 组件库,遵循以下规范:
组件库选择:优先使用
@vean/ui的预样式化S*组件(如SButton、SCard、STabs、STable、SInput、SModal等)样式引入:使用时在入口文件引入样式:
import '@vean/ui/styles.css';自动导入配置:通过
unplugin-vue-components配合UiResolver实现自动导入:import Components from 'unplugin-vue-components/vite'; import { UiResolver } from '@vean/ui/resolver'; Components({ resolvers: [UiResolver()] });主题配置:使用
SConfigProvider进行全局主题、尺寸、语言配置参考文档:
- 组件库 Skill:
https://github.com/soybeanjs/vean-ui/skills - 在线文档:
https://veanui.com/ - 组件参考:
https://veanui.com/llms.txt
- 组件库 Skill:
9.2 平台适配参考
各平台 (preset) 的适配实现必须优先参考以下开源项目:
Nitro (
/Users/soybean/Web/Projects/OpenSource/nitro)- 参考 Nitro 的 preset 架构设计
- 参考 Nitro 的平台能力检测与降级策略
- 参考 Nitro 的构建输出结构和 runtime 适配
- 重点关注:preset 定义、rollup 配置、runtime entry、平台特定 hooks
Hono Vite Plugins (
https://github.com/honojs/vite-plugins)- 参考 Hono 官方 Vite 插件实现
- 参考 dev server 集成模式
- 参考 HMR 和热重载策略
- 重点关注:vite-plugin 开发、dev 模式中间件、客户端注入
参考原则:
- 参考现有实现时学习架构设计与实现模式
- 保持 ubean 的 API 设计一致性
- 所有适配层必须有对应的测试用例
- 平台特定能力必须通过 capability 矩阵声明
9.3 依赖安装规范
- 使用
pnpm作为包管理器,遵循 workspace catalog 版本管理 - UI 相关依赖(@vean/ui 等)仅在需要时引入,不强制用户安装
- DevTools 相关依赖作为 devDependencies 或按需动态导入
- DevTools AI scaffold 为可选能力(ADR-0004):
ai/@ai-sdk/openai-compatible在@ubean/devtools中为optionalDependencies,运行时通过动态import()加载。未安装时框架与普通 DevTools 功能不受影响,仅触发 AI 助手功能时报清晰错误(含安装指引)。如需启用 AI 助手:pnpm add ai @ai-sdk/openai-compatible
10. CodeGraph 工作流约定
改动核心符号前,先用 CodeGraph 核查影响面,而非凭直觉或文档措辞估计。来源:ADR-0005。
10.1 何时执行
修改以下任一核心符号时,PR 描述必须附 codegraph impact 结果(简要 blast radius):
defineHandler/defineHandlerMeta/defineMiddleware(路由/API 处理器协议)scanProject(路由扫描)registerRoutes(路由注册)ubeanPlugin(Vite 插件主入口)macros(definePage等编译期宏)createUbeanApp/createUbeanClientApp(应用工厂)resolveModules(模块系统)
10.2 执行步骤
codegraph sync # 同步索引(packages/builder 已可索引,见 OPT-02)
codegraph impact <symbol> # 查影响面将输出中「直接引用 / 传递引用」计数与关键文件列表摘入 PR 描述。
10.3 与 PR 的关系
- 本约定先行:约定文本独立于代码 PR 落地。
- 首个样板:
createUbeanApp→createUbeanClientApp重命名 PR(OPT-01)作为首个遵循本约定的样板,PR 描述附codegraph impact createUbeanApp结果。 - 勿将
codegraph impact输出塞入本约定自身的非代码 PR——「定规」与「首用」分离。
11. 扩展包接入契约表
所有「扩展包」(有
./vite子路径导出 且不在主包ubean的dependencies中 的包)须在下表登记一行。CI(scripts/verify-packages.mjs,与包树校验共用脚本)会从packages/*/package.json派生扩展集,断言每个都在本表出现。来源:ADR-0006。注意:
pwa/fonts/electron/pinia/ui是@ubean/integrations的子路径(@ubean/integrations/pwa等),Vite 插件由子路径主入口导出,运行时会话辅助函数(如serializePiniaState/hydratePiniaState)由@ubean/integrations主入口导出。
11.1 契约表
| 包 | config key | /vite 插件 | runtime 入口 | peerDeps | 核心依赖形态 | 默认行为 |
|---|---|---|---|---|---|---|
@ubean/ai | ai | ubeanAiPlugin | ./runtime/vue (useChat/useAgent/useAIProvider) | ai, @ai-sdk/openai-compatible(optional), hono, vite, vue | optional-peer(ai/@ai-sdk/openai-compatible 在 peerDependencies 且 optional) | 薄封装 Vercel AI SDK;defineAgent/defineAgentTool + provider 预设;客户端自动导入 useChat/useAgent/useAIProvider |
@ubean/auth | auth | ubeanAuthPlugin | ./runtime (useAuth) | hono, vite, vue(均 optional) | hard(better-auth 在 dependencies) | 挂载 /api/auth/*;better-auth 优先,降级为内置 email/password |
@ubean/icon | icon | ubeanIconPlugin | ./runtime | vue(optional) | none(仅 defu/pathe) | Iconify customCollections;dev /_iconify 路由先本地 SVG 后回退 API |
@ubean/integrations/pwa | pwa | ubeanPwaPlugin(子路径主入口) | @ubean/integrations (usePwa) | vite, vue(均 optional) | hard(vite-plugin-pwa 在 dependencies) | 生成 manifest+sw;registerType: autoUpdate;5 种缓存策略 |
@ubean/image | image | ubeanImagePlugin | ./runtime | vite, vue(均 optional) | none(仅 defu/ohash/pathe/ufo) | 图片优化与变换 |
@ubean/content | content | ubeanContentPlugin | ./runtime + ./vue(useContentSearch) | vite, vue(均 optional) | none(仅 defu/pathe/scule + @ubean/shared) | markdown/MDX/YAML/JSON 内容集合;按标题层级切分搜索章节,SSG 产出 __search.json + 可选 Pagefind 索引 |
@ubean/integrations/fonts | fonts | ubeanFontsPlugin(子路径主入口) | @ubean/integrations | vite(optional) | none(仅 defu/ohash/pathe/ufo) | Google Fonts / 本地字体 / 自托管 / metrics |
@ubean/integrations/electron | electron | ubeanElectronPlugin(子路径主入口) | — | electron, vite(均 optional) | hard(vite-plugin-electron 在 dependencies) | 封装 vite-plugin-electron;electron: true 启用,自动禁用 SSR |
@ubean/integrations/pinia | pinia | ubeanPiniaPlugin(子路径主入口) | @ubean/integrations (serializePiniaState/hydratePiniaState) | pinia(optional), vue(optional) | optional-peer(pinia 在 peerDependencies 且 optional) | SSR 状态水合 + dev 预构建;不自动注入 Pinia 实例 |
@ubean/integrations/ui | ui | ubeanUiPlugin(子路径主入口) | — | @vean/ui(optional), vite(optional) | optional-peer(@vean/ui 在 peerDependencies 且 optional) | UiResolver 自动导入 + styles.css 注入(css: true 可关) |
11.2 核心依赖形态四值
- hard:核心库在
dependencies,安装扩展即自动安装(auth 与@ubean/integrations/pwa、@ubean/integrations/electron)。 - peer:核心库在
peerDependencies且非 optional,用户必须自行安装(当前扩展表里没有这一形态)。 - optional-peer:在
peerDependencies且optional: true(如各包对 vite/vue,以及pinia/@vean/ui)。 - none:无重核心库,仅工具函数依赖(icon/image/content/fonts)。
11.3 已识别的不一致
hard 与 optional-peer 并存是已知不一致:auth 与 @ubean/integrations/pwa、@ubean/integrations/electron 自动装核心库;@ubean/integrations/pinia、@ubean/integrations/ui 的核心库是 optional peer —— 不装也能安装扩展包,但启用对应能力时需自行安装。新增扩展包应明确选择一种并在本表登记;后续可视情况统一(见 ADR-0006)。
11.4 新增扩展包清单
新增扩展包 PR 必须同时:
- 在
package.json提供./vite子路径导出(使其被 CI 派生为扩展集); - 在本表 11.1 增加一行(缺行 CI 失败);
- 按 11.2 标注核心依赖形态。
12. 客户端 JS 预算
声称「更轻」必须带数字。构建之后运行:
pnpm --filter ubean-test build
pnpm analyze # 或 `ubean analyze`;读 dist/public/.vite/manifest.json默认把 gzip 汇总写到 .ubean/bundle-baseline.json(totalGzip / entryGzip / 各 chunk)。提交到仓库的回归基线是 examples/ubean-test/benchmarks/bundle-baseline.json(ubean analyze --out)。快照(2026-09-16,ubean-test 生产客户端):111.1 kB gzip 合计 / 45.2 kB entry(app-*.js)/ 32 个 JS chunk。Islands 默认页的回归以该文件为准,而不是印象。ubean analyze --write=false 只打印不写文件。CI 跑 ubean analyze --check benchmarks/bundle-baseline.json(默认允许合计 / entry gzip 相对增长 5%)。
两点容易踩的坑:
- 门禁也守「少产出」:除体积上限外还按名字对照基线的 chunk 名单 —— 基线里有、当前产物里没有的 chunk 直接失败(岛屿组件整类消失、体积反而变小,曾从这个门禁下溜过去)。
- 要固定
NODE_ENV:NODE_ENV=test时 Vite 尊重该值,客户端产物会打进 Vue 开发态代码(实测同一示例项目的 entry gzip 45.2 kB → 75.9 kB,文件数不变),门禁报的会是「体积回归」而真因是运行环境。ubean build遇到非 production 的NODE_ENV会打印警告。
除相对门禁外还可设绝对上限(只看本次构建,与基线无关,超出即失败):
ubean analyze --max-total-kb 160 --max-entry-kb 12 --max-chunk-kb 60三个值单位为 kB,可单独使用;--max-chunk-kb 超出时失败信息会列出 chunk 名与实测值。绝对上限与相对门禁并存,互不替代。
13. 生命周期性能基准
声称「更快」同样必须带数字。体积预算是确定性的、进 CI 阻塞;dev / build 生命周期延迟受机器噪声影响,不进 CI 阻塞,用于整改前后对照(docs/perf-regression-net.md)。
pnpm benchmark:lifecycle # 报告(默认 cli 臂;可用臂 cli / vite;warmup 1 + 5 次,报 p50/p95)
pnpm benchmark:lifecycle -- --runs 3 --warmup 1
pnpm benchmark:lifecycle -- --skip-browser # 跳过浏览器运行时指标
pnpm benchmark:lifecycle:baseline # 重新生成 examples/ubean-test/benchmarks/perf-baseline.json
pnpm benchmark:trend -- --report .temp/perf-report.json # 追加趋势点并渲染对比表采集四组指标:dev 冷启动、浏览器运行时(首个岛屿水合 / 站内导航)、变更生效延迟(服务端 / 客户端分开)、build 墙钟与峰值内存;另有 reload 正确性对照(改无关文件后模块实例是否保留)。原始样本与运行环境记录一并落盘,报告里的数字只有配上该文件才可复核。基线在 Vite 插件化之前 于旧路径上采集(RM-P05),是 vite-plugin-migration.md 风险 R3 与 RM-V13 / V15 / V23 的对照口径。
浏览器指标需要 Playwright 的 Chromium(npx playwright install chromium);缺失时报告会标注「不可用」并跳过该组,不影响其余指标。
周期性趋势:.github/workflows/nightly-perf.yml 每天跑一次基准,把点追加进 .temp/perf-trend.jsonl 并渲染「与上一次」「与 committed 基线」两张对比表到 job summary。它不设阈值、不因数值变化失败,也不在 PR 路径上。一处要留意:committed 基线在 darwin/arm64 上采集、nightly 在 ubuntu 上跑,趋势脚本会检测 platform/arch/cpuModel 是否一致,不同则把整段差值标为「机器不同、绝对值不可比,只看方向」—— 换机器时趋势表里会看到这条警告,那不是性能回归。
性能主张的纪律:任何声称性能收益的 PR 必须附可复现基准、正确性对照与前后数字;不接受「感觉没变慢」或「应该更快」。
下一步
- 运行时与开发体验 — dev server、预设与 CLI 命令系统
- 路由 — 文件式路由与路由规则
- 快速开始 — 几分钟内跑起一个项目
- ubean API 参考 — 核心运行时导出