Bruno 架构解析:Monorepo 布局、请求执行管线与 QuickJS 脚本沙箱
【免费下载链接】brunoOpensource IDE For Exploring and Testing API's (lightweight alternative to Postman/Insomnia)项目地址: https://gitcode.com/GitHub_Trending/br/bruno
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.json 的workspaces字段声明了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-app与bruno-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-schema与bruno-schema-types的分工、禁止向上依赖)定义在自动附加的规则文件.claude/rules/architecture.md中,区域级规则(.claude/rules/electron-ipc.md、.claude/rules/redux-store.md、.claude/rules/dsl-changes.md)则深入各主题细节。架构参考文档本身定位是"地图",具体约束以上述规则文件为准。
二、请求执行管线:从渲染进程到断言
一次"发送请求"动作的完整链路(以 Electron 应用为准):
- 渲染进程发起 IPC:渲染进程调用
ipcMain.handle('send-http-request', …),处理器位于 bruno-electron/src/ipc/network/index.js。 - 变量插值:
ipc/network/interpolate-vars.js调用@usebruno/common的interpolate完成{{variable}}替换。 - Pre-request 脚本:通过
ScriptRuntime执行(bruno-js/src/runtime/script-runtime.js)。 - 构建并发送请求:HTTP 走 axios,另有 gRPC(
@grpc/grpc-js)与 WebSocket 客户端。认证拦截器(如addDigestInterceptor、applyOAuth1ToRequest)、cookie 处理、scripting.buildScriptedEntry、grpc/ws 辅助等构件来自@usebruno/requests。需要注意:bruno-requests 是"请求构件库"而非编排器,真正的编排发生在ipc/network目录下(prepare-request.js、axios-instance.js、prepare-grpc-request.js、ws-event-handlers.js等文件共同完成这一层)。 - 响应后处理:变量提取由
VarsRuntime负责,测试由TestRuntime负责,断言由AssertRuntime负责——三者与ScriptRuntime是彼此独立的 runtime,均位于 bruno-js/src/runtime/(该目录现有script-runtime.js、test-runtime.js、assert-runtime.js、vars-runtime.js、scripted-entries.js五个文件)。 - 结果回传:结果以数据对象形式返回渲染进程,失败时对象上携带
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:vm的runInContext,位于 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.js:NODEVM_SCRIPT_WRAPPER_OFFSET/QUICKJS_SCRIPT_WRAPPER_OFFSET由前缀字符串的换行数计算得出,供错误格式化工具把 VM 报告的行号映射回.bru/.yml源码行。
脚本上下文中可用的全局对象(由script-runtime.js注入):
bru、req,以及res(仅 post-response 阶段);test、expect(chai)、assert(chai);console。
四、文件系统设计:.bru 与 .yml 双格式
bruno-filestore是解析/序列化的中心入口(bruno-filestore/src/index.ts),向下委托给formats/bru与formats/yml两个格式化模块。- 磁盘上存在两种格式:
.bru(Bruno 自有 DSL)与.yml(OpenCollection YAML)。新创建的集合默认使用.yml(DEFAULT_COLLECTION_FORMAT = 'yml',该常量定义于bruno-app的utils/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 | WebSocketRequest(requests/index.ts)。任何处理"一个请求"的代码都必须同时考虑这三种形态——这正是 Bruno 相比纯 HTTP 客户端在类型层面的关键约束。 - 其他中心类型:
Collection(collection/collection.ts)、Item(请求或文件夹,以type字段判别,collection/item.ts)、Auth(以mode为判别字段的联合类型,common/auth.ts)、KeyValue(headers/params/assertions 的通用键值对,common/key-value.ts)、Environment、Script。
运行时校验则由独立的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.json 中rollup被钉在3.30.0,axios被钉在1.18.0(另有tar、pbkdf2等安全相关固定)。在叶子包中提升这些版本号不会生效——必须改根级 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 语言特性。
八、测试资产:验证架构事实的入口
理解这套架构后,仓库中的测试资产是最直接的验证入口:
- packages/bruno-js/tests/:沙箱生命周期、QuickJS 陷阱隔离(
quickjs-trap-containment.spec.js)、teardown、脚本化条目等单测; - packages/bruno-electron/tests/:IPC 层测试(
network/目录覆盖请求准备、mock、代理等); - tests/ + playwright.config.ts:Playwright e2e,按项目分组运行(
test:e2e、test:e2e:ssl、test:e2e:auth、test:e2e:mock-server等脚本见根 package.json); - packages/bruno-tests/:express 测试服务器,支撑 auth、redirects、sse、graphql 等端到端场景。
结语
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。
【免费下载链接】brunoOpensource IDE For Exploring and Testing API's (lightweight alternative to Postman/Insomnia)项目地址: https://gitcode.com/GitHub_Trending/br/bruno
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考