news 2026/9/10 2:27:57

Danswer 移动端 Metro patch 实战:修复 react-native-worklets Bundle Mode 的 `.worklets` SHA-1 崩溃

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Danswer 移动端 Metro patch 实战:修复 react-native-worklets Bundle Mode 的 `.worklets` SHA-1 崩溃

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.3remend 1.3.0
react-native-worklets0.10.1worklets 运行时,Bundle Mode 编译产物输出到.worklets/目录
react-native-reanimated4.5.1Reanimated 4,peer 依赖react-native-worklets 0.10.x,与工作集共享同一 worklets 插件
metro0.84.5(经overrides锁定)通过@expo/metro间接引入的打包器

该工程的package.json配置了 bun 的原生补丁机制(见下节),补丁文件与说明文档一起放在mobile/patches/目录。

1.1 Bundle Mode 是什么

worklets 有两种工作模式:

  • Lazy Mode(默认):worklet 在 JS 运行时按需注册,Babel 插件只负责转换语法。
  • Bundle Modereact-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"] } }, ], ], }; };

注意注释中强调的两个约束:

  1. 同一时刻只能存在一个 worklets 插件babel-preset-expo默认会以无参数方式注册react-native-worklets/plugin,因此必须显式传worklets: false关闭它,再由自己以 Bundle Mode 参数注册——这个显式插件同时覆盖了 Reanimated 4。
  2. importForwarding.moduleNames: ["remend"]remendreact-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的:

  1. Metro 启动,DependencyGraph完成一次文件爬取,.worklets/目录中还没有任何产物;
  2. Babel 编译遇到 worklet 时,Bundle Mode 插件“即时(on the fly)”生成对应模块文件;
  3. Metro 在解析模块、计算内容哈希时调用DependencyGraph.getOrComputeSha1(path)
  4. 由于该路径从未进入过 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-configgetDefaultConfig构建配置。在 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" }

工作机制与安全约束:

  1. 自动重放patchedDependencies声明metro@0.84.5使用patches/metro@0.84.5.patch,bun 会在每次bun install时自动重新应用该补丁;
  2. 精确版本语义patchedDependencies的键名的是精确版本。若实际安装的 metro 版本与键名不符,bun既不应用补丁、也不打印任何警告——这是静默失败的隐患点;
  3. overrides兜底锁版:为了防止“版本漂移导致补丁静默失效”,overridesmetro钉死在0.84.5升级补丁时必须三处同步更新overrides中的版本、patchedDependencies的键名、以及补丁文件名本身,三者缺一不可。

在移动端工程的安装流程(见 mobile/README.md)中,bun install即会触发补丁应用:

bun install

(注意mobile/package.json还声明了preinstall钩子,会先构建../web/lib/shareddist,这是该工程file:依赖的配套机制。)

五、Metro 版本升级时如何重切补丁

当 Metro 跟随 Expo SDK 升级而升版时(例如从 0.84.5 升到 0.85.x),mobile/patches/README.md 给出了标准重切流程:

  1. 获取官方对应版本补丁:从 SWM 的react-native-worklets仓库bundleMode/patches/patch-package/metro目录取与目标 metro 版本匹配的metro+<version>.patch
  2. 用 bun 重切
    bun patch metro # 进入可编辑的补丁会话 # 应用上述 DependencyGraph.js 改动 bun patch --commit # 生成新的补丁文件
  3. 同步更新三处package.jsonoverrides版本、patchedDependencies键名、补丁文件名;
  4. 重新评估必要性:再次查看 metro issue #330 是否已在目标版本修复——如果已修复,直接删除补丁、移除patchedDependenciesoverrides中的对应条目即可。

文档还记录了一个有价值的细节: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.mockStreamingMarkdown打桩成() => 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目录是否加入watchFoldersmobile/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 2:27:43

职场人AI漫剧提效指南:轻量级视听叙事工作流

1. 职场人做漫剧不是“玩票”&#xff0c;而是时间成本的硬核博弈你有没有过这样的经历&#xff1a;下班后想用AI做个职场主题的漫剧小样&#xff0c;发在内部分享群或知识星球里——结果花3小时调参数、修提示词、等渲染&#xff0c;最后成片节奏拖沓、角色口型对不上、背景音…

作者头像 李华
网站建设 2026/9/10 2:26:54

断言、日志、异常、重试:企业级脚本稳定性四件套

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 2:26:21

KTV歌厅从设备选型到音响隔音调试的实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 2:24:51

C# SQLite加密数据库实战:AES-256增删改查闭环

简介&#xff1a;本资源是一个基于C# WinForm的SQLite数据库操作完整示例项目&#xff0c;面向.NET初学者与桌面应用开发者&#xff0c;聚焦数据安全与基础CRUD实践。项目实现了带密码保护的SQLite数据库创建、连接、增删改查等核心功能&#xff0c;并封装了SQLiteHelper工具类…

作者头像 李华