Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Pirsanth/cf201ee3b6cbae5acd504f5b03b20b29 to your computer and use it in GitHub Desktop.
Save Pirsanth/cf201ee3b6cbae5acd504f5b03b20b29 to your computer and use it in GitHub Desktop.
How to make React Native load dependencies from a custom path instead of node_modules

How to make React Native load dependencies from a custom path instead of node_modules

I've been a react-native developer for about one and a half years now. I have only recently begun reading some of react-native-community/cli's source code in preparation of becoming an open source contributor. In the process of doing so, I have found ways of alleviating a few common pain points for me when developing with react-native. This gist will deal with one such pain point, as per the title above and is a part of a larger series of such posts.

Story time

Link to the github example project

Working at my last company, eventually we had to make modifications to the native code of some of the third party react-native libraries we used. We needed to share these modifications (of files under node_modules) to the entire react-native team. We resorted to using patch-package to share the aforementioned changes.

I started this project to see if I can make react-native load these third-party dependencies from a custom path for example (./modifiedRNLibraries) in the project root so that:

  1. The libraries we modify and use would be explicit (no scouring through .gitignore for the libraries we are actually commiting under node_modules or going through ./patches for the libraries modified with patch-package)
  2. We would have more control over version control for these libraries

The solution

At react-native's project root we add a file called react-native.config.js that lists each (modified) third-party react-native library and the custom root you'd like to load each library from. A caveat to the root listed for each dependency name however is that the root needs to be an absolute path (details why below). For me to share the file with my team (can't use an hardcoded absolute path for that as our directory structures differ!), I simply have to convert the relative path to an absolute path dynamically using node's path module. An example of one such react-native.config.js file that declares a custom root for the react-native-image-with-progress-bar third-party library is given below:

const path = require('path');
const relativePathToLibraryFromRNProjectRoot = "./customPath/react-native-image-with-progress-bar";

module.exports = {
    dependencies: {
        "react-native-image-with-progress-bar": {
            root: path.resolve(__dirname, relativePathToLibraryFromRNProjectRoot)
        }
    },
    project: { ios: {}, android: {} },
    assets: [],
    commands: [],
    platforms: {}
  };
  

How the solution was found

Internally, the output of the react-native config command is used by both cocoapod1 and gradle2 to obtain information about the third party libraries to link. If we look at the source code for the react-native configcommand, we see that we can pass a custom root for a third-party react-native library using a react-native.config.js file at the root of the react-native project 3.

If the root passed in via react-native.config.js is an absolute path, like:

module.exports = {
    dependencies: {
        "react-native-image-with-progress-bar": {
             root: "/Users/pirsanth/Documents/reactNativeProject/customPath/react-native-image-with-progress-bar"
        }
    },
    project: { ios: {}, android: {} },
    assets: [],
    commands: [],
    platforms: {}
  };

we encounter no problems and the library from the custom root gets used when we run react-native run-android or cd ios&&pod install&&cd ..&&react-native run-ios(both from the RN project root). However we can't use an absolute path when we share the project amongst our work colleagues.

Using a relative path for Android

Using a relative path to the third-party library (relative to the RN project root) for root like:

//react-native.config.js at the RN project root

module.exports = {
    dependencies: {
        "react-native-image-with-progress-bar": {
             root: "./customPath/react-native-image-with-progress-bar"
        }
    },
    project: { ios: {}, android: {} },
    assets: [],
    commands: [],
    platforms: {}
  };

results in react-native-image-with-progress-bar not being linked at all (both the native code from ./customPath and ./node_modules/ is not linked) when react-native run-androidis ran. This is because when you use a relative path for root in react-native.config.js the output of the react-native config command changes based on the current working directory of the cli. When gradle runs the react-native config command, the current working directory is set to the RN project's android source directory 4.

Thus, when we use a relative path to the third-party library relative to the android source directory like so:

//react-native.config.js at the RN project root

module.exports = {
    dependencies: {
        "react-native-image-with-progress-bar": {
             root: "../customPath/react-native-image-with-progress-bar"
        }
    },
    project: { ios: {}, android: {} },
    assets: [],
    commands: [],
    platforms: {}
  };

and we run react-native run-android the library's native code in the custom path is used.

This approach works for android only: setting the relative path of the custom root of the third party library relative to the android source directory (./android in a normal react-native project). We will see in a later section how relative paths do not work as well for iOS.

Using a relative path for iOS

Similar to the above case for android, using root: "./customPath/react-native-image-with-progress-bar" (a relative path to the third-party RN library relative to the RN project root) results in the react-native-image-with-progress-bar dependency not being added by cocoapods at all. This is because the current working directory when the react-native config command is ran in native_modules.rb1 is the ios source directory (./ios in a normal react-native project). Note: I found out the current working directory by copying the lines below into native_modules.rb:

    Pod::UI.puts "Dir.pwd #{Dir.pwd}"

    IO.popen(["pwd"]) {|pwd_io|
      pwd_result_with_error = pwd_io.read
      Pod::UI.puts "pwd results #{pwd_result_with_error}"
    }

However, unlike the android case above, using root: "../customPath/react-native-image-with-progress-bar" (the relative path to the third party library from the ios source directory) results in an error when we run pod install in the ios directory. This happens because of this line (in native_modules.rb):

relative_path = podspec_dir_path.relative_path_from project_root 5

According to the Ruby Docs 6, in the above line the library's root (podspec_dir_path) and the project_root have to either both be relative paths or absolute paths. Furthermore, to accomodate the root of the library (podspec_dir_path) being a relative path, I will have to make both project_root and absolute_react_native_path relative paths as well due to 7.

Seeing as I'd have to modify so much to make the third-party library's root a hardcoded relative path on iOS, I decided to dynamically convert the relative paths to absolute paths as in the solution above to avoid unwanted side-effects. Besides, when there is no react-native.config.js file at all react-native config's output has all its paths as absolute paths by default.

Miscellaneous Notes

On having to run the pod install command after a third-party library's custom root change

If we change the root of a third-party library, we have to run pod install in the ios directory before running react-native run-ios. However for android, we can just run react-native run-android straight away.

On the two types of react-native.config.js files:

There are two types of react-native.config.js files: the react-native.config.js files placed at the root of a react-native project and the react-native.config.js files placed at the root of a third-party react-native library (for example ./node_modules/react-native-image-with-progress-bar). The format of the two files are as follows:

  • The base structure for react-native.config.jsat RN's project root:
  {
    dependencies: {},
    project: { ios: {}, android: {} },
    assets: [],
    commands: [],
    platforms: {}
  }
  • The base structure for react-native.config.jsat the root of a RN third party library (either under node_modules by default or your custom path as discussed above):
  {
    dependency: {
      platforms: { ios: {}, android: {} },
      assets: [],
      hooks: {},
      params: []
    },
    commands: [],
    platforms: {},
    healthChecks: []
  }

On the difference between the used native code and JS code of the third-party library:

Describing the situation through an example

I think that this is best explained with an example. Keeping the directory structure of the example project in mind (pictured above with a link to its github), if we modify react-native.config.js so as to use the ordinary (node_modules version of the library without a red background) version of the react-native-image-with-progress-bar library like so:

//the react-native.config.js file
//remember that we have to run `pod install` in the ios directory before running the `react-native run-ios` command

const path = require('path');
const relativePathToLibraryFromRNProjectRoot = "./modifiedRNLibraries/react-native-image-with-progress-bar";

module.exports = {
    dependencies: {
        // "react-native-image-with-progress-bar": {
        //     root: path.resolve(__dirname, relativePathToLibraryFromRNProjectRoot)
        // }
    },
    project: { ios: {}, android: {} },
    assets: [],
    commands: [],
    platforms: {}
  };
  

What will happen when we run either the react-native run-ios command or the react-native run-android command is that the native code from ./node_modules/react-native-image-with-progress-bar will be used, however the JS code from ./modifiedRNLibraries/react-native-image-with-progress-bar is used instead of the JS code in ./node_modules/react-native-image-with-progress-bar.

This was most surprising for the case where we do not specify a custom root for react-native-image-with-progress-bar in react-native.config.js as mentioned above but the same thing occurs when we do specify a custom root as well (by uncommenting the above snippet).

Why this occurs

For ios when we run pod install in the ios directory, the Podfile has the line require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' which loads the native_modules.rb file. The native_modules.rb file, inturn, uses the result of the react-native config command (which we can alter using the react-native.config.js file as described above) to add the native code of the react-native-image-with-progress-bar library (from either the custom root in ./modifiedRNLibraries/react-native-image-with-progress-bar or the default root of ./node_modules/react-native-image-with-progress-bar) via cocoapods

For android when we run the react-native run-android command the gradle wrapper's task that is ran uses the native_modules.gradle file to similarly run the react-native config command and add the native code of the react-native-image-with-progress-bar library (from either the custom root in ./modifiedRNLibraries/react-native-image-with-progress-bar or the default root of ./node_modules/react-native-image-with-progress-bar) via gradle.

However, on the JS side of things, babel will transpile the code in ./App.js and when the line corresponding to import ImageWithProgressBar from 'react-native-image-with-progress-bar'; in ./App.js is ran, the JS module resolution will occur. I don't know what module resolution algorithm react-native uses but it isn't the same as node's require()8. What we can infer from the above experiment with the example project though is that the JS module resolution algorithm seems to be unaffected by the changes in react-native.config.js. I might update this in the future as I read more of the source code and understand the module resolution algorithm better.


References

  1. How cocoapods uses react-native config's output
  2. How gradle uses react-native config's output
  3. How react-native config uses the information read from the react-native.config.js file at RN's project root to define a custom root to a third-party library
  4. Gradle runs the react-native config command with the cli's current working directory set to the RN project's android source directory
  5. Why pod install in the ios directory throws an error when a relative path is used for the RN third-party library's root
  6. Ruby Docs for Pathname's relative_path_from method
  7. Why we need to make absolute_react_native_path a relative path if we've made project_root a relative path
  8. Ben Nadal on how node's require looks for js modules
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment