react-nativemonorepolernayarn-workspacesmetro-bundler

React native couldn't resolve local module after noHoist has been added to project


I have this monorepo js setup with yarn workspaces and lerna

/package.json
/packages
   /common (js shared code)
     /package.json 
   /mobile (react native - metro)
     /package.json
   /web (CRA)
     /package.json

Mobile and web packages are importing common package inside package.json as follow

"dependencies": {
    "common": "*",
} 

I had to add noHoist option in root package.json so that mobile native dependencies don't get hoisted so build scripts still run fine

"workspaces": {
    "packages": [
      "packages/*"
    ],
    "nohoist": [
      "**/react-native",
      "**/react-native/**"
    ]
  }

Web did work fine before and after adding noHoist option

React native metro bundling start failing after adding noHoist .. it shows

"Error: Unable to resolve module .. could not be found within the project or in these directories:
  node_modules
  ../../node_modules" 

However common package does actually exists under root node_modules ? Looks like some kind of a linking issue ! (did try to link it manually/ same issue) .. note that I didn't add common package under noHoist

here how my metro config looks like

const path= require('path');

const watchFolders = [   
  path.resolve(`${__dirname}`), // Relative path to package node_modules   
  path.resolve(`${__dirname}/../../node_modules`), // Relative path to root node_modules ];

module.exports = {   
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),},
  maxWorkers: 2,
  watchFolders, };

ANY IDEA ? 🧐


Solution

  • Turns out the issue was in bundling, fixed by editing metro.config.js to include blocklist and extraNodeModules

    const path = require('path');
    const exclusionList = require('metro-config/src/defaults/exclusionList');
    const getWorkspaces = require('get-yarn-workspaces');
    
    function generateAssetsPath(depth, subpath) {
      return `/assets`.concat(
        Array.from({ length: depth })
          // eslint-disable-next-line no-unused-vars
          .map((_, i) => `/${subpath}`)
          .join(''),
      );
    }
    
    function getMetroAndroidAssetsResolutionFix(params = {}) {
      const { depth = 3 } = params;
      let publicPath = generateAssetsPath(depth, 'dir');
      const applyMiddleware = (middleware) => (req, res, next) => {
        // eslint-disable-next-line no-plusplus
        for (let currentDepth = depth; currentDepth >= 0; currentDepth--) {
          const pathToReplace = generateAssetsPath(currentDepth, 'dir');
          const replacementPath = generateAssetsPath(depth - currentDepth, '..');
          if (currentDepth === depth) {
            publicPath = pathToReplace;
          }
          if (req.url.startsWith(pathToReplace)) {
            req.url = req.url.replace(pathToReplace, replacementPath);
            break;
          }
        }
        return middleware(req, res, next);
      };
      return {
        publicPath,
        applyMiddleware,
      };
    }
    
    function getNohoistedPackages() {
      // eslint-disable-next-line global-require
      const monorepoRootPackageJson = require('../../package.json');
      const nohoistedPackages = monorepoRootPackageJson.workspaces.nohoist
        .filter((packageNameGlob) => !packageNameGlob.endsWith('**'))
        .map((packageNameGlob) => packageNameGlob.substring(3));
      return nohoistedPackages;
    }
    
    function getMetroNohoistSettings({
      dir,
      workspaceName,
      reactNativeAlias,
    } = {}) {
      const nohoistedPackages = getNohoistedPackages();
      const blockList = [];
      const extraNodeModules = {};
      nohoistedPackages.forEach((packageName) => {
        extraNodeModules[packageName] =
          reactNativeAlias && packageName === 'react-native'
            ? path.resolve(dir, `./node_modules/${reactNativeAlias}`)
            : path.resolve(dir, `./node_modules/${packageName}`);
        const regexSafePackageName = packageName.replace('/', '\\/');
        blockList.push(
          new RegExp(
            `^((?!${workspaceName}).)*\\/node_modules\\/${regexSafePackageName}\\/.*$`,
          ),
        );
      });
      return { extraNodeModules, blockList };
    }
    
    const workspaces = getWorkspaces(__dirname);
    
    const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix({
      depth: 3,
    });
    
    const nohoistSettings = getMetroNohoistSettings({
      dir: __dirname,
      workspaceName: 'mobile',
    });
    
    module.exports = {
      transformer: {
        // Apply the Android assets resolution fix to the public path...
        // publicPath: androidAssetsResolutionFix.publicPath,
        getTransformOptions: async () => ({
          transform: {
            experimentalImportSupport: false,
            inlineRequires: true,
          },
        }),
      },
      // server: {
      //   // ...and to the server middleware.
      //   enhanceMiddleware: (middleware) =>
      //     androidAssetsResolutionFix.applyMiddleware(middleware),
      // },
      // Add additional Yarn workspace package roots to the module map.
      // This allows importing importing from all the project's packages.
      watchFolders: [
        path.resolve(__dirname, '../../node_modules'),
        ...workspaces.filter((workspaceDir) => !(workspaceDir === __dirname)),
      ],
      maxWorkers: 2,
      resolver: {
        // Ensure we resolve nohoisted packages from this directory.
        blockList: exclusionList(nohoistSettings.blockList),
        extraNodeModules: nohoistSettings.extraNodeModules,
      },
    };
    

    You can check this universal CRA/RN mono-repo that uses such metro configs