Danswer 移动端 Metro patch 实战:修复 react-native-worklets Bundle Mode 的.workletsSHA-1 崩溃
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
在 Danswer(Onyx)移动端(mobile/目录)的 Expo + React Native 工程中,react-native-streamdown依赖 workletsBundle Mode,而该模式会在 Metro 首次文件爬取之后动态生成react-native-worklets/.worklets/*.js模块文件,导致 Metro 的DependencyGraph.getOrComputeSha1抛出Failed to get the SHA-1 for: …/.worklets/*.js(对应 facebook/metro 的 [issue #330])。本指南基于仓库中的 mobile/patches/README.md 与配套补丁 mobile/patches/metro@0.84.5.patch,完整讲解该崩溃的成因、bunpatchedDependencies的落地方式、补丁源码级原理,以及 Metro 版本升级时的重切流程,帮助你理解并复现这套修复方案。
一、背景:为什么移动端需要这个 patch
Danswer 移动端聊天界面需要在流式渲染场景下实时渲染 Markdown 消息(含代码块、引用、表格、[[n]](url)引用标记等)。该能力由 mobile/src/components/chat/StreamingMarkdown.tsx 中的StreamdownText(来自react-native-streamdown)承担:
import { StreamdownText } from "react-native-streamdown"; <StreamdownText markdown={content} markdownStyle={markdownStyle} flavor="github" selectable={!isStreaming} onLinkPress={onLinkPress ? (event) => onLinkPress(event.url) : undefined} />从 mobile/package.json 可以看到相关依赖的版本组合:
| 依赖 | 版本 | 作用 |
|---|---|---|
react-native-streamdown | ^0.2.0 | 流式 Markdown 渲染,peer 依赖react-native-worklets >= 0.8.3、remend 1.3.0 |
react-native-worklets | 0.10.1 | worklets 运行时,Bundle Mode 编译产物输出到.worklets/目录 |
react-native-reanimated | 4.5.1 | Reanimated 4,peer 依赖react-native-worklets 0.10.x,与工作集共享同一 worklets 插件 |
metro | 0.84.5(经overrides锁定) | 通过@expo/metro间接引入的打包器 |
该工程的package.json配置了 bun 的原生补丁机制(见下节),补丁文件与说明文档一起放在mobile/patches/目录。
1.1 Bundle Mode 是什么
worklets 有两种工作模式:
- Lazy Mode(默认):worklet 在 JS 运行时按需注册,Babel 插件只负责转换语法。
- Bundle Mode:
react-native-worklets/plugin在构建期把每个 worklet 编译成独立的模块文件,输出到node_modules/react-native-worklets/.worklets/*.js,并在运行时按模块引用加载。这是react-native-streamdown在流式场景下采用的模式,react-native-reanimated4.x 也基于同一套机制。
Bundle Mode 的启用方式在本仓库的 mobile/babel.config.js 中清晰可见:先关闭babel-preset-expo自动注入的 worklets 插件,再显式添加带bundleMode: true的插件配置:
module.exports = function (api) { api.cache(true); return { presets: [ // worklets:false 关闭 babel-preset-expo 自动添加的 worklets/plugin(默认无参数注册) ["babel-preset-expo", { jsxImportSource: "nativewind", worklets: false }], "nativewind/babel", ], plugins: [ // 必须保持在最后;importForwarding 把 remend 的 import 编译进 worklet bundle 本身, // 否则 remend 保持为外部引用,在 worklet 内调用会抛 // "Tried to synchronously call a Remote Function" 运行时错误 [ "react-native-worklets/plugin", { bundleMode: true, importForwarding: { moduleNames: ["remend"] } }, ], ], }; };注意注释中强调的两个约束:
- 同一时刻只能存在一个 worklets 插件。
babel-preset-expo默认会以无参数方式注册react-native-worklets/plugin,因此必须显式传worklets: false关闭它,再由自己以 Bundle Mode 参数注册——这个显式插件同时覆盖了 Reanimated 4。 importForwarding.moduleNames: ["remend"]:remend是react-native-streamdown的 peer 依赖,必须把它编译进 worklet bundle,否则从 worklet 内部调用 remend 会触发 “Tried to synchronously call a Remote Function” 运行时错误。
二、崩溃根因:Metro 文件爬取之后才出现的.worklets文件
2.1 时序问题
Metro 打包启动时会对源码目录做一次性文件爬取,建立文件地图(file map)。而 Bundle Mode 的 worklet 编译产物是在爬取完成之后、运行过程中才写入node_modules/react-native-worklets/.worklets/*.js的:
- Metro 启动,
DependencyGraph完成一次文件爬取,.worklets/目录中还没有任何产物; - Babel 编译遇到 worklet 时,Bundle Mode 插件“即时(on the fly)”生成对应模块文件;
- Metro 在解析模块、计算内容哈希时调用
DependencyGraph.getOrComputeSha1(path); - 由于该路径从未进入过 Metro 的文件地图,哈希计算直接抛异常:
Failed to get the SHA-1 for: /…/node_modules/react-native-worklets/.worklets/xxx.js
这正是 facebook/metro issue #330 描述的问题,且 SWM(Software Mansion,worklets/Reanimated 维护方)官方确认:截至 metro 0.85.0 均未修复,官方将其定位为“临时代用方案(temporary workaround)”,直到必要改动合入 Metro 主线。
2.2 为什么只 patchmetro而不 patch@expo/metro
本仓库的 mobile/metro.config.js 使用expo/metro-config的getDefaultConfig构建配置。在 Expo 工程中,@expo/metro只是一个再导出(re-export)垫片(module.exports = require("metro/...")),它本身不包含任何 metro 源码。因此真正的 Metro 实现存在于裸的metro包中——patch 裸metro即可全局生效,@expo/metro无需也不能被打补丁。
2.3 配套措施:把.worklets目录加入 watchFolders
补丁负责“计算哈希不抛错”,而mobile/metro.config.js还做了配套工作——把.worklets目录加入watchFolders,让 Metro 能感知到这些动态产物并参与监视:
const workletsDir = path.resolve( __dirname, "node_modules/react-native-worklets/.worklets", ); config.watchFolders = [...(config.watchFolders ?? []), sharedRoot, workletsDir];不过代码注释明确提醒:仅把目录加入watchFolders是不够的,它不能单独解决哈希计算问题——真正的修复仍依赖 SWM 官方 Metro 补丁。此外metro.config.js中还对react-native-worklets/.worklets/*的模块解析做了定向路由:先调用getBundleModeMetroConfig(config)拿到 Bundle Mode 的resolveRequest,再在包装函数里只对.worklets/前缀走 Bundle Mode 解析器,其余保持默认解析,且该包装必须在withNativeWind之前完成,以保证 NativeWind 的 css-interop 解析器能链式捕获到它。
三、补丁内容逐行解析
补丁文件 只改动 Metro 源码中的一个文件:src/node-haste/DependencyGraph.js。补丁分两处:
第一处:模块顶部引入目录常量
+const workletsDirPath = _path.default.join( + "react-native-worklets", + ".worklets", +);用一个平台无关的路径拼接方式定义目标目录特征串。用_path.join而不是硬编码分隔符,是为了保证在 Windows(\)与 Unix(/)下都能正确匹配mixedPath。
第二处:在getOrComputeSha1开头短路返回合成哈希
async getOrComputeSha1(mixedPath) { + if (mixedPath.includes(workletsDirPath)) { + const createHash = require("crypto").createHash; + return { + sha1: createHash("sha1") + .update(performance.now().toString()) + .digest("hex"), + }; + } const result = await this._fileSystem.getOrComputeSha1(mixedPath); if (!result || !result.sha1) { throw new Error(`Failed to get the SHA-1 for: ${mixedPath}.核心逻辑拆解:
- 命中判断:
mixedPath.includes(workletsDirPath)匹配任何包含react-native-worklets/.worklets路径片段的文件; - 合成哈希:以
performance.now().toString()作为输入,用 Node 内置crypto计算 SHA-1。由于performance.now()每次调用值都不同,每次生成的哈希都“新鲜”,既满足 Metro 对返回结构{ sha1 }的约定,又避免多个.worklets文件共享相同哈希; - 短路返回:命中后直接
return,完全绕过this._fileSystem.getOrComputeSha1(mixedPath)及后续的抛错分支。
补丁保持返回结构与 Metro 原始实现完全一致({ sha1: string }),因此对 Metro 其余逻辑完全透明。
四、bunpatchedDependencies:补丁如何被应用
本仓库使用 bun 的补丁机制(而非 patch-package)。在 mobile/package.json 中有两个互相配合的键:
"patchedDependencies": { "metro@0.84.5": "patches/metro@0.84.5.patch" }, "overrides": { "metro": "0.84.5" }工作机制与安全约束:
- 自动重放:
patchedDependencies声明metro@0.84.5使用patches/metro@0.84.5.patch,bun 会在每次bun install时自动重新应用该补丁; - 精确版本语义:
patchedDependencies的键名的是精确版本。若实际安装的 metro 版本与键名不符,bun既不应用补丁、也不打印任何警告——这是静默失败的隐患点; overrides兜底锁版:为了防止“版本漂移导致补丁静默失效”,overrides把metro钉死在0.84.5。升级补丁时必须三处同步更新:overrides中的版本、patchedDependencies的键名、以及补丁文件名本身,三者缺一不可。
在移动端工程的安装流程(见 mobile/README.md)中,bun install即会触发补丁应用:
bun install(注意mobile/package.json还声明了preinstall钩子,会先构建../web/lib/shared的dist,这是该工程file:依赖的配套机制。)
五、Metro 版本升级时如何重切补丁
当 Metro 跟随 Expo SDK 升级而升版时(例如从 0.84.5 升到 0.85.x),mobile/patches/README.md 给出了标准重切流程:
- 获取官方对应版本补丁:从 SWM 的
react-native-worklets仓库bundleMode/patches/patch-package/metro目录取与目标 metro 版本匹配的metro+<version>.patch; - 用 bun 重切:
bun patch metro # 进入可编辑的补丁会话 # 应用上述 DependencyGraph.js 改动 bun patch --commit # 生成新的补丁文件 - 同步更新三处:
package.json的overrides版本、patchedDependencies键名、补丁文件名; - 重新评估必要性:再次查看 metro issue #330 是否已在目标版本修复——如果已修复,直接删除补丁、移除
patchedDependencies与overrides中的对应条目即可。
文档还记录了一个有价值的细节:src/node-haste/DependencyGraph.js从 metro 0.84.4 到 0.84.5逐字节一致,因此从 0.84.4 重切到 0.84.5 时补丁内容无需任何改动(本仓库的补丁正是从 0.84.4 版重切的)。
另外注意补丁路径的差异:SWM 官方分发的是 patch-package 格式(diff 路径为node_modules/metro/...),而 bun 格式要求包相对路径(src/...)。本仓库补丁只重写了 diff 中的路径前缀,不改变任何实际内容,因此最终得到的 metro blob 哈希与 SWM 官方一致——这一点在 mobile/patches/README.md 中作为验证手段被专门强调。
六、仓库配套验证与工程约束
6.1 worklets 与测试的隔离
由于react-native-streamdown/worklets 在 jest(Node 环境)下无法运行,仓库在测试侧做了隔离,这从侧面印证了 worklets 的运行时特性:
- mobile/src/chat/timeline/textStats.ts:注释明确说明
textStats被刻意排除在 sheet 之外、保持“reanimated-free 且可单元测试”,因为引入 sheet 会连带引入StreamingMarkdown → worklets从而 crash jest; - mobile/src/components/chat/renderers/tests/findRenderer.test.ts:通过
jest.mock把StreamingMarkdown打桩成() => null,避免传递引入 worklets; - mobile/src/components/chat/tests/AgentTimeline.test.tsx:mock 掉
StreamingMarkdown,改用普通Text渲染 content 以便断言。
6.2 本工程其余 Metro 配置要点
除了 worklets 相关的补丁与配置,mobile/metro.config.js 还体现了与共享包协作的关键约束:
@onyx-ai/shared位于web/lib/shared(monorepo 共享包),需加入watchFolders并开启unstable_enablePackageExports以支持其子路径导出(./nativewind-theme、./native);resolver.nodeModulesPaths锚定到mobile/node_modules,保证整个工程只有一份 React/RN 实例;- 通过
blockList屏蔽web/lib/shared/node_modules,避免 Haste/重复模块冲突; - 顺序要求:
getBundleModeMetroConfig必须先于withNativeWind执行,withNativeWind会保留watchFolders并链式包装 Bundle Mode 的resolveRequest。
七、小结与排查清单
当你在任何 Expo + worklets Bundle Mode 工程中遇到Failed to get the SHA-1 for: …/.worklets/*.js时,可以按以下清单自查(参考本仓库的完整落地):
| 检查项 | 依据(仓库路径) |
|---|---|
补丁是否随bun install生效 | mobile/package.json 的patchedDependencies+overrides |
Babel 是否只注册了一个、且带bundleMode: true的 worklets 插件 | mobile/babel.config.js |
.worklets目录是否加入watchFolders | mobile/metro.config.js |
Bundle ModeresolveRequest是否在withNativeWind之前挂载 | mobile/metro.config.js |
升级 Metro 时三处版本是否同步(overrides/patchedDependencies/ 文件名) | mobile/patches/README.md |
这套“官方补丁 + bun 精确版本锁 + 工程配置配套”的组合拳,是 Danswer 移动端在 Expo 57 / Metro 0.84.5 / worklets 0.10.1 组合下稳定运行流式 Markdown 渲染的关键基础设施。只要 Metro 主线尚未合入正式修复(截至 0.85.0),升级任何相关依赖时都应把上述检查项纳入发布流程。
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考