react-nativenpmsymlink

How to use symlinks in React Native project?


The symlink support is still not officially available in react-native https://github.com/facebook/metro/issues/1.

It's actually possible to use symlinks in the package.json with npm (not yarn)

{
  "name": "PROJECT",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest"
  },
  "dependencies": {
    "my_module1": "file:../shared/my_module1/",
    "my_module2": "file:../shared/my_module2/",
    "react": "16.8.3",
    "react-native": "0.59.5",
  },
  "devDependencies": {
    "babel-jest": "24.7.1",
    "jest": "24.7.1",
    "metro-react-native-babel-preset": "0.53.1",
    "react-test-renderer": "16.8.3"
},
"jest": {
    "preset": "react-native"
  }
}

Although we will get my_module1 does not exist in the Haste module map

To fix this we could do before a metro.config.js (formerly rn-cli.config.js)

const path = require("path")

const extraNodeModules = {
  /* to give access to react-native-firebase for a shared module for example */
  "react-native-firebase": path.resolve(__dirname, "node_modules/react-native-firebase"),
}
const watchFolders = [
  path.resolve(__dirname, "node_modules/my_module1"),
  path.resolve(__dirname, "node_modules/my_module2"),
]

module.exports = {
  resolver: {
    extraNodeModules
  },
  watchFolders,
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false
      }
    })
  }
}

Unfortunately it doesn't work anymore on react-native 0.59 The app is reloading, but changes in the source code are not reflected in the app. Anyone has a clue to achieve this?


Solution

  • After a few years working on this, we found a reliable way using yarn.

    For react-native < 0.74

    Add your local dependencies with are inside a folder link:../CompanyPackages/package e.g:

    // package.json
    "dependencies": {
      "local-package": "link:../CompanyPackages/local-package"
    }
    

    Use a custom metro.config.js

    const path = require("path")
    const { mapValues } = require("lodash")
    
    // Add there all the Company packages useful to this app
    const CompanyPackagesRelative = {
      "CompanyPackages": "../CompanyPackages",
    }
    
    const CompanyPackages = mapValues(CompanyPackagesRelative, (relativePath) =>
      path.resolve(relativePath)
    )
    
    function createMetroConfiguration(projectPath) {
      projectPath = path.resolve(projectPath)
    
      const watchFolders = [...Object.values(CompanyPackages)]
      const extraNodeModules = {
        ...CompanyPackages,
      }
    
      // Should fix error "Unable to resolve module @babel/runtime/helpers/interopRequireDefault"
      // See https://github.com/facebook/metro/issues/7#issuecomment-508129053
      // See https://dushyant37.medium.com/how-to-import-files-from-outside-of-root-directory-with-react-native-metro-bundler-18207a348427
      const extraNodeModulesProxy = new Proxy(extraNodeModules, {
        get: (target, name) => {
          if (target[name]) {
            return target[name]
          } else {
            return path.join(projectPath, `node_modules/${name}`)
          }
        },
      })
    
      return {
        projectRoot: projectPath,
        watchFolders,
        resolver: {
          transform: {
            experimentalImportSupport: false,
            inlineRequires: true,
          },
          extraNodeModules: extraNodeModulesProxy,
        },
      }
    }
    
    module.exports = createMetroConfiguration(__dirname)
    

    For react-native => 0.74

    We are still looking for a fix