You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Turborepo Monorepo切换Expo Router遇fs模块解析错误求助

问题:Turborepo Monorepo中Expo切换到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 package C:\Users\monorepo\node_modules\fs\package.json is invalid because it specifies a main module 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 14:02:03