工程规范、测试与发布

5.1 核心原则

  1. 纯函数优先: 所有工具函数必须是纯函数,相同输入产生相同输出,无副作用
  2. 不可变性: 使用 const、readonly、Object.freeze()、展开运算符而非突变
  3. 函数组合: 使用 pipe/compose 模式组合函数,避免深层嵌套
  4. 高阶函数: 使用高阶函数抽象通用模式
  5. 类型安全: 严格 TypeScript,避免 any,充分利用泛型
  6. 无类设计: 优先使用工厂函数和闭包而非 class;必要时仅在运行时核心使用 class (如 Router、DevServer)
  7. 核心纯函数: 路由解析、配置归并、代码生成与类型计算保持纯函数;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 测试类型

  1. 单元测试 (test/unit/)
    • 纯函数测试: 工具函数、配置解析、路由匹配
    • 不依赖文件系统或网络
    • 快速执行,覆盖率目标 > 90%
  2. 集成测试 (test/integration/)
    • 构建流程测试
    • 开发服务器测试
    • Preset 适配测试
    • 使用 test/fixtures 中的完整项目
  3. 浏览器端到端测试 (test/e2e/)
  • 使用 Playwright 验证 SSR hydration、客户端导航、表单 action 与错误页
  • 只对正式支持的 preset 执行,不以模拟器替代正式平台 smoke test
  1. 类型测试
    • 使用 expectTypeOf 验证类型推导
  2. 打包与部署 smoke test
  • pnpm pack 后在独立 fixture 安装并验证公开 exports、CLI 与类型声明
  • 对每个正式 preset 执行 dev、build、preview 和目标平台部署 smoke test

6.3 持续验收门槛

测试不是最后阶段的收尾任务。每个实现阶段都必须新增或更新对应 fixture,并在合并前满足以下门槛:

  1. 单元测试覆盖新增的纯计算、扫描和代码生成逻辑。
  2. 至少一个真实 fixture 覆盖新增能力的 dev、build 和 preview 路径。
  3. 公开 TypeScript API 添加正反类型测试;生成文件变更需验证增量更新与冷启动结果一致。
  4. 改动 SSR、路由或页面协议时添加浏览器端到端测试。
  5. 改动 preset 能力时更新能力矩阵,并运行该 preset 的本地或远程 smoke test。
  6. 改动客户端传输时,覆盖 ofetch 默认路径、XHR FormData 上传进度、取消、超时、未知 total、HTTP/网络错误归一化,以及 SSR/edge 的不支持诊断。

覆盖率用于发现盲区,不作为替代契约测试的发布标准。核心运行时与公开 API 默认纳入统计;仅生成代码、平台不可执行的 shim 和经批准的适配器分支可排除,并在配置旁说明原因。

6.4 当前验证基线(2026-09-10)

  • 主包(ubean)自身无测试套件 —— 测试位于其所验证的子包中。
  • 核心子包:@ubean/server 382、@ubean/builder 234、@ubean/islands 205(directive / paired-components / server-client-components / islands-registry / server-component-rerender)、@ubean/vue 202、@ubean/routes 152(含 Server Actions)、@ubean/client 130、@ubean/cli 96、@ubean/config 95、@ubean/devtools 41、@ubean/preset 88。
  • 扩展包:@ubean/icon 32、@ubean/auth 15、@ubean/image 45、@ubean/content 93(含 30 个全文搜索测试)、@ubean/integrations 39(pwa / fonts)、@ubean/seo 116、@ubean/markdown 21、@ubean/i18n 22、@ubean/pages 63、@ubean/scan 13、@ubean/shared 54、@ubean/ai 15、@ubean/app 69。
  • 示例:ubean-test 783(含 prerender fixture)、client-only-spa 30。
  • 全仓库合计 3035 个测试通过。
  • pnpm typecheck:通过。编译器版本由 pnpm-workspace.yaml 中的 workspace override typescript: '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 + 内联 defineConfigIsomorphic:契约上浏览器安全——不含 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/ssrSSR 渲染器: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.1Node.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 组件库,遵循以下规范:

  1. 组件库选择:优先使用 @vean/ui 的预样式化 S* 组件(如 SButton、SCard、STabs、STable、SInput、SModal 等)

  2. 样式引入:使用时在入口文件引入样式:

    import '@vean/ui/styles.css';
  3. 自动导入配置:通过 unplugin-vue-components 配合 UiResolver 实现自动导入:

    import Components from 'unplugin-vue-components/vite';
    import { UiResolver } from '@vean/ui/resolver';
    
    Components({
      resolvers: [UiResolver()]
    });
  4. 主题配置:使用 SConfigProvider 进行全局主题、尺寸、语言配置

  5. 参考文档:

    • 组件库 Skill: https://github.com/soybeanjs/vean-ui/skills
    • 在线文档: https://veanui.com/
    • 组件参考: https://veanui.com/llms.txt

9.2 平台适配参考

各平台 (preset) 的适配实现必须优先参考以下开源项目:

  1. Nitro (/Users/soybean/Web/Projects/OpenSource/nitro)

    • 参考 Nitro 的 preset 架构设计
    • 参考 Nitro 的平台能力检测与降级策略
    • 参考 Nitro 的构建输出结构和 runtime 适配
    • 重点关注:preset 定义、rollup 配置、runtime entry、平台特定 hooks
  2. Hono Vite Plugins (https://github.com/honojs/vite-plugins)

    • 参考 Hono 官方 Vite 插件实现
    • 参考 dev server 集成模式
    • 参考 HMR 和热重载策略
    • 重点关注:vite-plugin 开发、dev 模式中间件、客户端注入
  3. 参考原则:

    • 参考现有实现时学习架构设计与实现模式
    • 保持 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/aiaiubeanAiPlugin./runtime/vue (useChat/useAgent/useAIProvider)ai, @ai-sdk/openai-compatible(optional), hono, vite, vueoptional-peer(ai/@ai-sdk/openai-compatible 在 peerDependencies 且 optional)薄封装 Vercel AI SDK;defineAgent/defineAgentTool + provider 预设;客户端自动导入 useChat/useAgent/useAIProvider
@ubean/authauthubeanAuthPlugin./runtime (useAuth)hono, vite, vue(均 optional)hard(better-auth 在 dependencies)挂载 /api/auth/*;better-auth 优先,降级为内置 email/password
@ubean/iconiconubeanIconPlugin./runtimevue(optional)none(仅 defu/pathe)Iconify customCollections;dev /_iconify 路由先本地 SVG 后回退 API
@ubean/integrations/pwapwaubeanPwaPlugin(子路径主入口)@ubean/integrations (usePwa)vite, vue(均 optional)hard(vite-plugin-pwa 在 dependencies)生成 manifest+sw;registerType: autoUpdate;5 种缓存策略
@ubean/imageimageubeanImagePlugin./runtimevite, vue(均 optional)none(仅 defu/ohash/pathe/ufo)图片优化与变换
@ubean/contentcontentubeanContentPlugin./runtime + ./vue(useContentSearch)vite, vue(均 optional)none(仅 defu/pathe/scule + @ubean/shared)markdown/MDX/YAML/JSON 内容集合;按标题层级切分搜索章节,SSG 产出 __search.json + 可选 Pagefind 索引
@ubean/integrations/fontsfontsubeanFontsPlugin(子路径主入口)@ubean/integrationsvite(optional)none(仅 defu/ohash/pathe/ufo)Google Fonts / 本地字体 / 自托管 / metrics
@ubean/integrations/electronelectronubeanElectronPlugin(子路径主入口)—electron, vite(均 optional)hard(vite-plugin-electron 在 dependencies)封装 vite-plugin-electron;electron: true 启用,自动禁用 SSR
@ubean/integrations/piniapiniaubeanPiniaPlugin(子路径主入口)@ubean/integrations (serializePiniaState/hydratePiniaState)pinia(optional), vue(optional)optional-peer(pinia 在 peerDependencies 且 optional)SSR 状态水合 + dev 预构建;不自动注入 Pinia 实例
@ubean/integrations/uiuiubeanUiPlugin(子路径主入口)—@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 必须同时:

  1. 在 package.json 提供 ./vite 子路径导出(使其被 CI 派生为扩展集);
  2. 在本表 11.1 增加一行(缺行 CI 失败);
  3. 按 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 必须附可复现基准、正确性对照与前后数字;不接受「感觉没变慢」或「应该更快」。


下一步