首页
/ Bruno 架构解析:Monorepo 布局、请求执行管线与 QuickJS 脚本沙箱

Bruno 架构解析:Monorepo 布局、请求执行管线与 QuickJS 脚本沙箱

2026-09-07 16:52:47作者:房伟宁

Bruno 是一个以"文件即 API 集合"为核心理念的开源 API 客户端(Postman/Insomnia 的轻量替代)。本文基于仓库内的架构参考文档 .claude/reference/architecture.md,结合各包的实际源码与 package.json,完整梳理 Bruno 的 npm workspaces monorepo 布局、跨进程请求执行管线(渲染进程 → Electron 主进程 → axios/gRPC/WebSocket)、bruno-js 的双模脚本沙箱(QuickJS / Node VM)、.bru/.yml 双文件格式系统,以及关键依赖的版本约束。读完后,你将能够准确定位任意功能所属的包、判断编辑某个包后是否需要重新构建、并理解一次请求从发起到断言执行的完整调用链。

一、Monorepo 结构:17 个 workspace 的分工

Bruno 使用 npm workspaces 组织代码,根 package.jsonworkspaces 字段声明了 packages/ 下的 16 个包目录(含 bruno-sqlite),另有仓库根级的 tests/(Playwright e2e 测试)与 playwright/(测试夹具与辅助函数)。

各包的定位与 npm 包名如下:

目录 npm 包名 职责
packages/bruno-app/ @usebruno/app React 渲染进程(rsbuild、Redux Toolkit、styled-components)
packages/bruno-electron/ bruno未加 scope,其余均为 @usebruno/* Electron 主进程,网络请求的实际执行者(electron-builder 打包)
packages/bruno-js/ 用户脚本沙箱(QuickJS + node:vm
packages/bruno-common/ @usebruno/common 共享工具,依赖 DAG 的底座(rollup → dist/cjs+dist/esm
packages/bruno-converters/ @usebruno/converters 导入/导出(Postman、Insomnia、OpenAPI 等)
packages/bruno-requests/ @usebruno/requests HTTP/gRPC/WS 请求的共享构件(rollup)
packages/bruno-filestore/ @usebruno/filestore .bru/.yml 序列化(rollup + tsc --emitDeclarationOnly
packages/bruno-query/ @usebruno/query JSONPath 风格查询引擎(rollup)
packages/bruno-lang/ Bru DSL 语法:v1(arcsecond,遗留)+ v2(ohm-js,当前)
packages/bruno-schema/ @usebruno/schema Yup 运行时校验(collections/requests)
packages/bruno-schema-types/ @usebruno/schema-types 仅 TypeScript 类型定义(tsc,types-only)
packages/bruno-graphql-docs/ @usebruno/graphql-docs GraphQL 文档生成器(rollup)
packages/bruno-toml/ @iarna/toml 的薄封装(当前未在任何活跃数据路径上使用)
packages/bruno-cli/ 命令行执行器,直接从 src/ 运行
packages/bruno-tests/ 测试服务器(express:HTTP/HTTPS/proxy/GraphQL)
packages/bruno-docs/ @usebruno/docs 占位 stub(仅 package.json + readme,无 src/)

补充:当前仓库的 workspaces 中还新增了 packages/bruno-sqlite@usebruno/sqlite),已被 bruno-appbruno-electron 作为依赖引入,承担本地 SQLite 持久化相关能力,架构文档的包列表略早于该包的纳入。

构建工具差异:哪些包需要重新构建

每个包的构建工具不同,这直接决定"编辑后要不要重跑构建":

  • rollup(产出 dist/cjs + dist/esm):bruno-common、bruno-converters、bruno-requests、bruno-query、bruno-graphql-docs;其中 bruno-filestore 额外执行 tsc --emitDeclarationOnly(已核实其 build 脚本为 rollup -c && tsc --emitDeclarationOnly -p tsconfig.build.json)。
  • 仅 tsc:bruno-schema-types(build 脚本为 tsc -p tsconfig.json)。
  • rsbuild:bruno-app。
  • 无构建步骤(直接消费 src/:bruno-js、bruno-lang、bruno-schema、bruno-toml、bruno-cli、bruno-electron、bruno-tests、bruno-docs。

因此编辑后必须重新构建的 7 个包(因为它们产出 dist/)是:bruno-common、bruno-requests、bruno-filestore、bruno-converters、bruno-query、bruno-graphql-docs、bruno-schema-types。编辑 bruno-js / bruno-lang / bruno-schema / bruno-toml 则无需重建——这些包以源码形式被直接消费。

依赖方向与所有权边界

内部 @usebruno/* 依赖 DAG 的所有权护栏(bruno-common 保持浏览器安全、bruno-js 不依赖 Electron API、bruno-schemabruno-schema-types 的分工、禁止向上依赖)定义在自动附加的规则文件 .claude/rules/architecture.md 中,区域级规则(.claude/rules/electron-ipc.md.claude/rules/redux-store.md.claude/rules/dsl-changes.md)则深入各主题细节。架构参考文档本身定位是"地图",具体约束以上述规则文件为准。

二、请求执行管线:从渲染进程到断言

一次"发送请求"动作的完整链路(以 Electron 应用为准):

  1. 渲染进程发起 IPC:渲染进程调用 ipcMain.handle('send-http-request', …),处理器位于 bruno-electron/src/ipc/network/index.js
  2. 变量插值ipc/network/interpolate-vars.js 调用 @usebruno/commoninterpolate 完成 {{variable}} 替换。
  3. Pre-request 脚本:通过 ScriptRuntime 执行(bruno-js/src/runtime/script-runtime.js)。
  4. 构建并发送请求:HTTP 走 axios,另有 gRPC(@grpc/grpc-js)与 WebSocket 客户端。认证拦截器(如 addDigestInterceptorapplyOAuth1ToRequest)、cookie 处理、scripting.buildScriptedEntry、grpc/ws 辅助等构件来自 @usebruno/requests。需要注意:bruno-requests 是"请求构件库"而非编排器,真正的编排发生在 ipc/network 目录下(prepare-request.jsaxios-instance.jsprepare-grpc-request.jsws-event-handlers.js 等文件共同完成这一层)。
  5. 响应后处理:变量提取由 VarsRuntime 负责,测试由 TestRuntime 负责,断言由 AssertRuntime 负责——三者与 ScriptRuntime彼此独立的 runtime,均位于 bruno-js/src/runtime/(该目录现有 script-runtime.jstest-runtime.jsassert-runtime.jsvars-runtime.jsscripted-entries.js 五个文件)。
  6. 结果回传:结果以数据对象形式返回渲染进程,失败时对象上携带 error 字段——handler 不 reject Promise,这是 Electron IPC 的错误约定(详见 .claude/rules/electron-ipc.md)。

ipc/network 目录本身就是这条管线的源码级索引:

packages/bruno-electron/src/ipc/network/
  index.js                 # send-http-request 等 IPC handler 总入口
  interpolate-vars.js      # 第 2 步:变量插值
  prepare-request.js       # 第 4 步:请求准备
  axios-instance.js        # HTTP 客户端封装
  prepare-grpc-request.js  # gRPC 分支
  ws-event-handlers.js     # WebSocket 分支
  grpc-event-handlers.js   # gRPC 事件处理
  awsv4auth-helper.js      # AWS v4 签名认证

三、脚本沙箱(bruno-js):QuickJS 与 Node VM 双模

用户脚本(pre-request / post-response / tests)在两种沙箱中执行:

  • QuickJS(默认,"safe" 模式):基于 quickjs-emscripten 的 WebAssembly 沙箱,实现位于 bruno-js/src/sandbox/quickjs/。用户脚本被包裹在异步闭包中,setTimeout 被替换为基于 bru.sleep 的异步实现,从而在纯 QuickJS 环境中提供计时语义;
  • Node VM("developer" 模式)node:vmrunInContext,位于 bruno-js/src/sandbox/node-vm/,可访问更完整的 Node 能力。

运行时的选择是集中式的。getJsSandboxRuntime 读取集合的 securityConfig.jsSandboxMode 配置完成映射:'safe'(默认)→ QuickJS,'developer' → Node VM。主进程侧的实现(bruno-electron/src/ipc/network/index.js)为:

const getJsSandboxRuntime = (collection) => {
  const securityConfig = get(collection, 'securityConfig', {});

  if (securityConfig.jsSandboxMode === 'developer') {
    return 'nodevm';
  }

  // default runtime is `quickjs`
  return 'quickjs';
};

CLI 侧(bruno-cli/src/commands/run.js)也实现了同名函数,保证命令行执行与桌面应用行为一致。架构文档明确要求:新增沙箱模式逻辑必须走 getJsSandboxRuntime,不要重复推导映射关系

两种沙箱的脚本包裹前缀与行号偏移量统一维护在 bruno-js/src/utils/sandbox.jsNODEVM_SCRIPT_WRAPPER_OFFSET / QUICKJS_SCRIPT_WRAPPER_OFFSET 由前缀字符串的换行数计算得出,供错误格式化工具把 VM 报告的行号映射回 .bru/.yml 源码行。

脚本上下文中可用的全局对象(由 script-runtime.js 注入):

  • brureq,以及 res(仅 post-response 阶段);
  • testexpect(chai)、assert(chai);
  • console

四、文件系统设计:.bru 与 .yml 双格式

  • bruno-filestore 是解析/序列化的中心入口bruno-filestore/src/index.ts),向下委托给 formats/bruformats/yml 两个格式化模块。
  • 磁盘上存在两种格式.bru(Bruno 自有 DSL)与 .yml(OpenCollection YAML)。新创建的集合默认使用 .ymlDEFAULT_COLLECTION_FORMAT = 'yml',该常量定义于 bruno-apputils/common/constants 并被"新建集合/保存临时请求"等 UI 流程引用)。
  • 当前 .bru 语法是 v2(ohm-js),位于 bruno-lang/v2/src/,filestore 通过 bruToJsonV2 / jsonToBruV2 使用它;v1(arcsecond,位于 bruno-lang/v1/src/)为遗留实现。
  • 集合直接存放在文件系统上,由 Electron 主进程用 chokidar 监听变更。

任何对磁盘形状(on-disk shape)的变更,都需要遵循 .claude/rules/dsl-changes.md 中定义的流程。

五、应用侧:Redux Store 与 Providers

渲染进程的状态管理采用 Redux Toolkit。权威 slice 清单见 bruno-app/src/providers/ReduxStore/index.js 中的 reducer 映射(约 11 个 slice,其中 collections/ 目录是体量最大的一块)。中间件约定与副作用边界由 .claude/rules/redux-store.md 规定。

Provider 层(bruno-app/src/providers/)当前包含:App/ReduxStore/Theme/Hotkeys/Toaster/PromptVariables/ 等目录,分别负责应用生命周期、全局状态、主题、快捷键、轻提示与交互式变量输入。

六、核心数据模型类型(bruno-schema-types)

核心类型集中在 bruno-schema-types/src/,包内只有类型定义(build 仅执行 tsc),供全仓库共享。理解 Bruno 数据模型必须先掌握以下事实:

  • 请求是一个联合类型Request = HttpRequest | GrpcRequest | WebSocketRequestrequests/index.ts)。任何处理"一个请求"的代码都必须同时考虑这三种形态——这正是 Bruno 相比纯 HTTP 客户端在类型层面的关键约束。
  • 其他中心类型:Collectioncollection/collection.ts)、Item(请求或文件夹,以 type 字段判别,collection/item.ts)、Auth(以 mode 为判别字段的联合类型,common/auth.ts)、KeyValue(headers/params/assertions 的通用键值对,common/key-value.ts)、EnvironmentScript

运行时校验则由独立的 bruno-schema(Yup)承担——"类型定义"与"运行时校验"分属两个包,这一分工是所有权边界的一部分。

七、关键依赖版本:以当前 package.json 为准

架构文档特别提醒:多个核心依赖被钉在低于最新的大版本上,不要假设它们是最新 API。以下版本已逐一在当前仓库各 package.json 中核实(适用前提:以本次仓库快照为准):

前端(bruno-app,见 packages/bruno-app/package.json

依赖 当前版本 注意
react / react-dom 19.0.0
@reduxjs/toolkit ^1.8.0 v1,不是 v2
react-redux ^7.2.9 v7
styled-components ^5.3.3 不是 v6
codemirror 5.65.2 CodeMirror 5,不是 scoped 的 @codemirror/*
tailwindcss ^3.4.1 v3
@rsbuild/core ^1.7.6 devDependency

桌面端(bruno-electron,见 packages/bruno-electron/package.json

依赖 当前版本
electron ~37.6.1(devDependency)
electron-builder ^24.13.3(devDependency)
chokidar ^3.5.3
@grpc/grpc-js ^1.14.4
js-yaml 4.3.1
electron-store ^8.1.0

ws 是 bruno-requests / bruno-tests 的依赖,不是 bruno-electron 的直接依赖。

解析层(bruno-lang)arcsecond ^5(v1,遗留)、ohm-js ^16.6(v2,当前);bruno-toml 封装 @iarna/toml 但当前未被使用。

根 package.json 的硬钉(overrides)package.jsonrollup 被钉在 3.30.0axios 被钉在 1.18.0(另有 tarpbkdf2 等安全相关固定)。在叶子包中提升这些版本号不会生效——必须改根级 override。

TypeScript 版本不统一:bruno-common 为 ^5.8.3,bruno-schema-types 为 ^5.0.0,而 bruno-converters / bruno-filestore / bruno-graphql-docs / bruno-query / bruno-requests 均为 ^4.8.4;根目录没有任何 TS 依赖。跨包编写类型代码时不能假设全局一致的 TS 语言特性。

八、测试资产:验证架构事实的入口

理解这套架构后,仓库中的测试资产是最直接的验证入口:

结语

Bruno 的架构可以概括为一条清晰的主轴:bruno-app(渲染)与 bruno-electron(主进程)构成 Electron 双进程外壳,ipc/network 是请求编排的唯一中枢,bruno-requests 提供可复用构件,bruno-js 的四个独立 runtime 分别承担 pre-request、变量提取、测试与断言,bruno-filestore + bruno-lang 守护 .bru/.yml 双格式,bruno-schema-types 则以 HttpRequest | GrpcRequest | WebSocketRequest 三态联合类型约束全仓库的请求处理代码。开发时牢记三条护栏:编辑 7 个产 dist/ 的包后必须重新构建、依赖升级要改根级 override 而非叶子包、所有沙箱模式逻辑必须收敛到 getJsSandboxRuntime

登录后查看全文
热门项目推荐
相关项目推荐