)
Tamagui 单仓库实战Yarn hoisting 限制与 Metro 模块解析的取舍kitchen-sink 工程经验【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui导读在 Tamagui 开源仓库中code/kitchen-sink 是一个同时面向 Webwebpack与原生Expo/Metro的测试与演示应用它直接以workspace:*方式依赖数十个 monorepo 内部包因此对依赖提升hoisting与模块解析module resolution极为敏感。仓库根目录的 code/kitchen-sink/README.md 记录了一个极具代表性的工程陷阱在单仓库中运行yarn ios需要在 package.json 中设置installConfig.hoistingLimits: dependencies但该设置会让 Metro 无法构建 JS除非在 metro.config.js 中把config.resolver.nodeModulesPaths指向 monorepo 根目录。本文以此为骨架结合metro.config.js、tamagui/metro-plugin源码与测试脚本完整还原这一对矛盾的产生原因、解决路径与工程启示帮助你在自己的 Tamagui 单仓库中避开同类坑。一、背景为什么 kitchen-sink 对依赖解析如此敏感kitchen-sink 不是普通的示例应用而是一个面向 Tamagui 全量组件与动画驱动器的回归测试场它的dependencies以workspace:*协议引用tamagui/core、tamagui/web、tamagui/config、tamagui/sheet、tamagui/toast等约 30 个内部包见 code/kitchen-sink/package.json版本统一为2.7.7它在入口 code/kitchen-sink/index.js 中按顺序导入tamagui/native/setup-zeego、setup-teleport、setup-gesture-handler、setup-keyboard-controller、setup-burnt等原生能力并要求tamagui.config在任何组件导入之前先被 require这意味着模块被解析到哪个副本、按什么顺序加载直接决定运行时行为它同时维护了 190 个 Playwright Web 测试见 code/kitchen-sink/tests与 Detox/原生测试run-detox.sh、code/kitchen-sink/run-native-tests.sh单一依赖被错误提升都可能导致测试间出现幽灵差异。从仓库根 package.json 的workspaces字段可以看到kitchen-sink 与 compiler、core、ui、demos 等几十个工作区包同处一个仓库根目录还有overrides对react-native、babel/core等做了强制版本覆盖。在这个结构下每个工作区能否正确找到自己该用的依赖版本是工程正确性的第一道门槛。二、第一则笔记yarn ios为何需要hoistingLimits: dependenciesREADME 的第一条结论是yarn iosfails unless you add to package.json:installConfig: { hoistingLimits: dependencies }即要让yarn iosExpo 原生构建跑通需要在kitchen-sink 的 package.json中加入{ installConfig: { hoistingLimits: dependencies } }它的作用机理这是 Yarn BerryYarn 2的installConfig.hoistingLimits配置用于限制 Yarn 对依赖的提升层级默认workspaces下Yarn 会尽量把公共依赖提升到仓库根node_modules避免重复安装设置为dependencies时只有声明为直接 dependencies 的包才会被提升到顶层node_modules而传递依赖间接依赖保留在各自更深的层级中。为什么这能让yarn ios通过因为在 Expo 的原生工具链CocoaPods、Xcode 构建中react-native、expo等关键原生依赖如果被过度提升或与项目声明的版本不一致很容易出现版本错位、重复链接、头文件冲突。把提升范围收窄到直接依赖可以保证react-native/expo严格按 kitchen-sink 声明的版本0.86.2/~57.0.10解析减少根目录下同时存在多个 React Native 副本导致的链接歧义让原生模块如react-native-reanimated、react-native-worklets与其 peer 依赖相对位置更稳定。需要特别说明的是这条笔记是仓库在 Yarn 时代沉淀的经验。当前仓库已切换为 Bun 工作区根 package.json 中packageManager: bun1.4.0kitchen-sink 为bun1.3.9installConfig只对 Yarn 生效但它揭示的问题——原生构建要求依赖位置可控——至今仍然成立理解它有助于你迁移到任何包管理器时做出等价约束。三、第二则笔记Metro 的反制与nodeModulesPaths的兜底README 的第二条紧接着给出了一个但是but metro fails to build js unless you remove this (and make sureconfig.resolver.nodeModulesPathsis set to monorepo root in metro.config).也就是说保持hoistingLimits: dependencies会让 Metro 构建 JS 失败除非移除该配置在支持 Metro 的流程中在 code/kitchen-sink/metro.config.js 中把config.resolver.nodeModulesPaths显式设置为 monorepo 根目录。为什么 Metro 会失败Metro 解析模块时按nodeModulesPaths给出的顺序逐个目录查找。当 Yarn 把传递依赖压回各自工作区深处时Metro 默认只从项目自身目录向上查找无法覆盖 monorepo 根下集中安装的依赖于是出现形如Unable to resolve module react-native的报错。README 的潜台词是hoisting 限制与 Metro 的查找范围是一对天然冲突必须让其中一方让步或显式对齐。kitchen-sink 的完整解法仓库实际采用的 metro.config.js 是一套单仓库 Metro 标准配置把上面的约束拆成了四步const { getDefaultConfig } require(expo/metro-config) const path require(path) const { withTamagui } require(tamagui/metro-plugin) const projectRoot __dirname // 1. monorepo 根 项目目录向上两级 const monorepoRoot path.resolve(projectRoot, ../..) const config getDefaultConfig(projectRoot) config.resolver.unstable_enablePackageExports process.env.TAMAGUI_PACKAGE_EXPORTS ! false // watchman 本地不稳定且对 CI 无帮助 - 关闭 config.resolver.useWatchman false // 屏蔽无关目录减少 Metro 文件爬取 config.resolver.blockList [ /code\/tamagui\.dev\//, /code\/.*\/__tests__\//, /code\/.*\/\.maestro\//, ] // 2. 让 Metro 监听整个 monorepo config.watchFolders [monorepoRoot] // 3. 显式声明模块查找顺序先项目自身再 monorepo 根 config.resolver.nodeModulesPaths [ path.resolve(projectRoot, node_modules), path.resolve(monorepoRoot, node_modules), ] // ...resolveRequest 兜底逻辑见下文 module.exports withTamagui(config, { components: [tamagui], config: ./src/tamagui.config.ts, })其中最关键的就是nodeModulesPaths第一优先级是code/kitchen-sink/node_modules项目自己的依赖第二优先级是 monorepo 根node_modules被提升的公共依赖。这正是 README 所说的 set to monorepo root。层级化 node_modules 的兜底解析仓库还在该文件中实现了一个更激进的兜底拦截resolveRequest当默认解析失败时手工构造分层级 node_modules 路径列表再重试一次config.resolver.resolveRequest (context, moduleName, platform) { try { return context.resolveRequest(context, moduleName, platform) } catch (e) { const hierarchicalNodeModulesPaths path .dirname(context.originModulePath) .split(path.sep) .map((_, i, parts) path.join(/, ...parts.slice(1, parts.length - i), node_modules) ) return context.resolveRequest( { ...context, nodeModulesPaths: [...hierarchicalNodeModulesPaths, ...context.nodeModulesPaths], }, moduleName, platform ) } }这段代码解决的是文件注释中提到的实际问题Metro 对 monorepo 根node_modules下的嵌套 node_modules解析存在缺陷——例如从node_modules/parse5导入entities时本应解析到node_modules/parse5/node_modules/entitiesMetro 却错误地直接解析到根node_modules/entities。兜底逻辑按originModulePath逐级上溯生成候选路径把node_modules的查找还原为 Node 语义。文件注释也诚实地标注了它的边界它只修复构建期错误无法修复因错误解析导致的运行时错误因此它只是缓解而非根治。何时用哪套综合 README 与源码可以总结出清晰的取舍矩阵场景做法原因yarn iosYarn 时代原生构建保留installConfig.hoistingLimits: dependencies收紧提升范围稳定原生依赖位置Metro JS 构建移除该配置或保证nodeModulesPaths指向 monorepo 根让 Metro 能找到被压深的依赖两者都要保留 hoisting 限制 nodeModulesPaths双路径 resolveRequest兜底显式对齐解析范围牺牲部分性能换正确性四、源码佐证withTamagui与 Babel 编译器在此配置下的角色Metro 配置最后一行调用了tamagui/metro-plugin的withTamagui。查看 code/compiler/metro-plugin/src/index.ts 可以确认它在上述解析链路之上还做了两件事把css追加进resolver.sourceExts让 Metro 能解析 Tamagui 的 CSS 产物加载 Tamagui 构建配置loadTamaguiBuildConfigSync并把解析后的选项挂到config.transformer.tamagui上供 Transformer 侧使用。同时code/kitchen-sink/babel.config.js 展示了 JS 侧的另一条编译路径默认启用tamagui/babel-plugincomponents: [tamagui, tamagui/sandbox-ui]config: ./src/tamagui.config.ts并支持通过DISABLE_COMPILER环境变量跳过编译器以加速构建例如 Detox 测试场景。这说明 kitchen-sink 的构建是Metro 解析 Babel 编译器两条链路叠加任何一条链路上的模块解析错误都会表现为启动或测试失败。五、如何验证与复现这一配置的影响kitchen-sink 提供了多套可验证入口便于你亲手确认依赖解析行为原生启动在仓库根目录执行bun run kitchen-sink对应 package.json 的kitchen-sink脚本或进入code/kitchen-sink后执行bun run start:ios/start:android需要原生原生构建产物时用bun run ios内部先执行./pod-install.shWeb 启动bun run start:webwebpack dev server与bun run start:web:extract开启静态提取DISABLE_EXTRACTIONfalse原生测试bun run test:native:ios、test:native:android、test:native:maestro它们统一经由code/packages/native-ci/src/cli.ts驱动Web 测试bun run test:web并行执行 Playwright 用例tests目录下的*.test.tsx覆盖了 Sheet、Popover、Menu、Tooltip 等组件的解析与交互回归。如果某次修改动了 hoisting 或nodeModulesPaths最直接的回归手段就是同时跑一遍test:native与test:web观察是否出现 Unable to resolve module 或组件行为漂移。六、结论给 Tamagui 单仓库使用者的三条经验把依赖位置当成一等配置项。hoistingLimits与nodeModulesPaths不是玄学而是原生工具链与 JS 打包器对依赖位置的两种不同假设。凡是同时面向 iOS 与 Metro 的单仓库应用都需要像 kitchen-sink 这样把nodeModulesPaths显式声明为「项目自身 monorepo 根」并准备好resolveRequest兜底。理解约束之间的优先级。README 的但是句是全篇核心没有一个全局开关能同时满足两条链路取舍取决于你当前在跑哪条命令。把这种取舍以注释或笔记形式固化在 package.json / metro.config.js 旁边是团队协作成本最低的做法。把测试当验收标准。kitchen-sink 之所以敢把依赖压深是因为它有tests目录下近 200 个 Playwright 用例与 native-ci 驱动测试作为兜底。在你的项目中任何依赖解析配置的改动都应配套回归测试否则构建过了但运行时行为漂移的隐性风险会随时间累积。输出文章【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考