Turborepo Monorepo切换Expo Router遇fs模块解析错误求助
在Turborepo管理的monorepo开发React Expo项目,从@react-navigation/native-stack切换到expo-router后,遇到fs模块解析失败的错误,报错信息如下:
Web Bundling failed 11748ms
frenchies-spots-webui:dev: error: Error: The packageC:\Users\monorepo\node_modules\fs\package.jsonis invalid because it specifies amainmodule field that could not be resolved (C:\Users\monorepo\node_modules\fs\index.js. None of these files exist:
C:\Users\monorepo\node_modules\fs\index.js(.web.ts|.ts|.web.tsx|.tsx|.web.js|.js|.web.jsx|.jsx|.web.json|.json)
尝试修改metro.config.js中的sourceExts配置但无效,以下是当前项目的核心配置文件:
当前配置文件
metro.config.js
const { getDefaultConfig } = require("expo/metro-config"); const path = require("path"); // Find the workspace root, this can be replaced with `find-yarn-workspace-root` const workspaceRoot = path.resolve(__dirname, "../.."); const projectRoot = __dirname; const config = getDefaultConfig(projectRoot); // 1. Watch all files within the monorepo config.watchFolders = [workspaceRoot]; config.transformer = { getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, inlineRequires: true, }, }), }; // 2. Let Metro know where to resolve packages, and in what order config.resolver = { nodeModulesPaths: [ path.resolve(projectRoot, "node_modules"), path.resolve(workspaceRoot, "node_modules"), ], sourceExts: ["mjs", "jsx", "js", "ts", "tsx", "cjs", "json", "cjx"], // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` disableHierarchicalLookup: true, }; module.exports = config;
package.json
{ "name": "frenchies-spots-webui", "version": "1.0.0", "scripts": { "start": "expo start --clear", "android": "expo start --clear --android", "ios": "expo start --clear --ios", "web": "expo start --clear --web", "eject": "expo eject", "dev": "expo start --clear --web", "build": "npx expo export:web" }, "dependencies": { "@apollo/client": "^3.8.1", "@babel/preset-env": "^7.1.6", "@expo/match-media": "^0.4.0", "@expo/ngrok": "4.1.0", "@frenchies-spots/gql": "workspace:*", "@frenchies-spots/materials": "workspace:*", "@react-native-async-storage/async-storage": "1.18.2", "core-js": "^3.29.1", "expo": "^49.0.3", "expo-cli": "6.3.2", "expo-constants": "~14.4.2", "expo-font": "~11.4.0", "expo-image-picker": "~14.3.2", "expo-keep-awake": "~12.3.0", "expo-linking": "~5.0.2", "expo-location": "~16.1.0", "expo-modules-autolinking": "~1.5.0", "expo-router": "2.0.0", "expo-screen-orientation": "~6.0.5", "expo-splash-screen": "~0.20.4", "expo-status-bar": "~1.6.0", "graphql": "^16.8.0", "metro": "~0.73.3", "metro-react-native-babel-preset": "0.77.0", "metro-resolver": "0.73.9", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.72.4", "react-native-gesture-handler": "~2.12.0", "react-native-reanimated": "~3.3.0", "react-native-safe-area-context": "4.6.3", "react-native-screens": "~3.22.0", "react-native-web": "~0.19.6" }, "devDependencies": { "@babel/core": "^7.19.3", "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@expo/webpack-config": "^18.0.1", "@types/lodash": "^4.14.191", "@types/react": "~18.2.14", "@types/react-native": "^0.69.15", "@types/webpack": "^5.28.0", "typescript": "5.1.3" }, "overrides": { "metro": "0.76.0", "metro-resolver": "0.76.0" }, "private": true }
webpack.config.js
const path = require("path"); const createExpoWebpackConfigAsync = require("@expo/webpack-config"); function getWorkspaceModules() { const { dependencies } = require("./package.json"); return Object.entries(dependencies) .filter(([, v]) => v.startsWith("workspace:")) .map(([k]) => k); } module.exports = async function (env, argv) { const config = await createExpoWebpackConfigAsync( { ...env, babel: { ...env.babel, dangerouslyAddModulePathsToTranspile: getWorkspaceModules(), }, }, argv ); // Modify the config object with watch options config.watch = true; config.watchOptions = { aggregateTimeout: 200, poll: 1000, }; config.externals = { "react-native": true, }; return config; };
babel.config.js
module.exports = function (api) { api.cache(true); return { presets: ["babel-preset-expo"], plugins: [ "expo-router/babel", "@babel/plugin-proposal-export-namespace-from", "react-native-reanimated/plugin", ], }; };
解决方案
1. 移除不必要的fs依赖
fs是Node.js核心模块,Expo项目不需要单独安装。执行命令删除:
npm uninstall fs # 或 yarn remove fs
2. 修复Webpack配置的externals问题
当前把react-native设为external会导致Expo Web无法正确解析RN相关模块,删除以下配置:
config.externals = { "react-native": true, };
3. 调整Metro配置映射Node核心模块
在metro.config.js的resolver中添加extraNodeModules,将Node核心模块映射到Expo兼容实现或空模块:
config.resolver = { // 保留原有配置 nodeModulesPaths: [ path.resolve(projectRoot, "node_modules"), path.resolve(workspaceRoot, "node_modules"), ], sourceExts: ["mjs", "jsx", "js", "ts", "tsx", "cjs", "json", "cjx"], disableHierarchicalLookup: true, // 添加核心模块映射 extraNodeModules: { fs: require.resolve("expo-file-system"), // 使用Expo的文件系统模块 // 若不需要fs功能,可映射到空模块:require.resolve('empty-module') }, };
如果使用空模块,先安装依赖:
npm install empty-module --save-dev
4. 清理缓存并重装依赖
确保依赖和缓存干净:
# 删除依赖和锁文件 rm -rf node_modules package-lock.json yarn.lock # 重装依赖 npm install # 清除Expo缓存并启动 npx expo start --clear
5. 检查workspace模块的Node核心模块引用
检查@frenchies-spots/gql和@frenchies-spots/materials两个workspace模块,确认是否引入了fs等Node核心模块。如果有,修改为Expo/Web兼容实现,或通过babel/webpack在构建时替换这些模块。
内容的提问来源于stack exchange,提问作者JF-PS

