DEV Community

loading...

Fixing React Native issues and happy deploy to Bitrise, Fabric, CircleCI

Khoa Pham
My apps https://onmyway133.com/apps/
・14 min read

This post is about implementing QR code scanning, fixing building issues, continuous integration with Bitrise, crash reporting with Fabric and how to tame React Native projects into source control. React Native is hard, but dealing with dependencies is harder. Just after a short while I collect tons of issues, most of them are well known issues, but some take a while to figure out. Let’s start with a simple QR scanner feature.
What is create-react-native-app ?
*As someone who comes to React Native from iOS and Android background, I like React and Javascript as much as I like…*medium.com

QR code scanner with react-native-camera

After integrating Facebook and Firebase, my app needs QR code scanner feature. A quick search shows several libraries, but all of them seem to be just a wrapper around react-native-camera.

In an ideal world, we can just run npm install react-native-camera --save and react-native link react-native-camera to have a camera feature. We of course need to tweak a bit for each platform. Firstly with permission on Android, and NSCameraUsageDescription on iOS. Then there are some issues. The version I use is react-native-camera 1.2.0 . Future version of the library will hopefully fix the below bugs, but we all have to be ready for other bugs.

Duplicate module name: react-native

Loading dependency graph...(node:15556) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: jest-haste-map: @providesModule naming collision:
  Duplicate module name: react-native
  Paths: /Users/khoa/XcodeProject2/MyApp/node_modules/react-native/package.json collides with /Users/khoa/XcodeProject2/MyApp/ios/Pods/React/package.json

This error is caused by a @providesModule declaration with the same name across two different files.
(node:34363) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Enter fullscreen mode Exit fullscreen mode

This clearly states that there are 2 module with the same name react-native . Because react-native-camera use CocoaPods to install a project called RNCamera , somehow they brings with a pod React , which is why there is Users/khoa/XcodeProject2/MyApp/ios/Pods/React/package.json , which contains the name of react-native .

A quick fix is to change the name to something else, for example

"name": "react-native-avoid-collision"
Enter fullscreen mode Exit fullscreen mode

Argument list too long

Argument list too long: recursive header expansion failed at /Users/*k*hoa/XcodeProject2/MyApp/node_modules/react-native-camera/ios/../../../ios/build
Enter fullscreen mode Exit fullscreen mode

This is because of recursive search paths in framework search path. If you open RNCamera.xcodeproj at node_modules/react-native-camera/ios/RNCamera.xcodeproj/project.pbxproj and look for Framework search path under Build settings, you can see that it is currently recursive . You should change $(SRCROOT)/../../ios/to non-recursive for both Debug and Release configuration.

The library com.google.android.gms:play-services-basement is being requested by various other libraries

org.gradle.api.GradleException: The library com.google.android.gms:play-services-basement is being requested by various other libraries at [[15.0.1,15.0.1]], but resolves to 12.0.1. Disable the plugin and check your dependencies tree using ./gradlew :app:dependencies.
Enter fullscreen mode Exit fullscreen mode

Because I use Firebase, it has conflict with react-native-camera. Simply because it needs Firebase for face and text detection. In my opinion this is nice to have, but break the single responsibility principle. A camera library should have camera feature only !!!

GMV (Google Mobile Vision) is used for Face detection/Text recognition by the iOS RNCamera. You have to link the google frameworks to your project to successfully compile the RNCamera project.

A quick fix is to specify ext { googlePlayServicesVersion = "15.0.1" } to the project build.gradle

The usage is quite straightforward

render() {
  return (
    <View style={styles.container}>
      <Camera
        style={styles.camera}
        aspect={Camera.constants.Aspect.fill}
        onBarCodeRead={this.onBarCodeRead}
        ref={cam => this.camera = cam}
      />
      {this.makeOverlayIfAny()}
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

The onBarCodeRead is where we handle callback for QR code detection.

OK, all the issues seem to be solved. Let’s see how it builds on CI.

Running React Native apps on Bitrise

I had good experience with Bitrise for Android and iOS app, and since it also supports React Native, there’s no double in using it. In theory, we can just add add a React Native app and Bitrise can build iOS and Android apps. But in practice, there’s always issues.

Cannot find module ‘jest-haste-map’

My app runs perfectly locally, but fails on Bitrise. I have contacted support but clueless. Since Bitrise supports Workflow with lots of steps, I can choose Run npm command to install missing module

After running npm install jest-haste-map , I get

Cannot find module 'jest-worker'
Enter fullscreen mode Exit fullscreen mode

Then I run npm install jest-worker , I get

Cannot find module 'jest-serializer'
Enter fullscreen mode Exit fullscreen mode

Then I run npm install jest-serializer , I get

Loading dependency graph...(node:74975) UnhandledPromiseRejectionWarning: Error: fseventsunavailable (this watcher can only be used on Darwin)
at new FSEventsWatcher
Enter fullscreen mode Exit fullscreen mode

main.jsbundle does not exist

The other issue I get is

main.jsbundle does not exist. This must be a bug with + echo 'React Native.
Enter fullscreen mode Exit fullscreen mode

This mean there’s an error during bundling, so the main.jsbundle could not be found. Since it runs fine locally, I think I should manually generate bundle and track that to Git, since react-native init by default adds jsbundle in .gitignore . Let’s add the following to package.json , remember that the name field must be all lowercase and not contains spaces.

"build:ios": "react-native bundle --entry-file ./index.js --platform ios --bundle-output ios/main.jsbundle"
Enter fullscreen mode Exit fullscreen mode

It still fails on Bitrise. And with newer version of React, this is not needed, as React Native a bundling script in Xcode Build Phrase that can detect configuration and whether we use index.js or index.ios.js

# Define entry file
if [[ -s "index.ios.js" ]]; then
  ENTRY_FILE=${1:-index.ios.js}
else
  ENTRY_FILE=${1:-index.js}
fi
Enter fullscreen mode Exit fullscreen mode

if [[ "$CONFIGURATION" = "Debug" && ! "$PLATFORM_NAME" == *simulator ]]; then
  IP=$(ipconfig getifaddr en0)
  if [ -z "$IP" ]; then
    IP=$(ifconfig | grep 'inet ' | grep -v ' 127.' | cut -d\   -f2  | awk 'NR==1{print $1}')
  fi

echo "$IP" > "$DEST/ip.txt"
fi

BUNDLE_FILE="$DEST/main.jsbundle"

$NODE_BINARY "$CLI_PATH" $BUNDLE_COMMAND \
  $CONFIG_ARG \
  --entry-file "$ENTRY_FILE" \
  --platform ios \
  --dev $DEV \
  --reset-cache \
  --bundle-output "$BUNDLE_FILE" \
  --assets-dest "$DEST" \
  $EXTRA_PACKAGER_ARGS
Enter fullscreen mode Exit fullscreen mode

For Android, before React Native 0.19.0 we needed to manually run below to generate bundle.

react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/
Enter fullscreen mode Exit fullscreen mode

Bundling React Native during Android release builds
*Automating the bundling of js resources for React Native during Android release builds*proandroiddev.com

Now we don’t need to manually run React Native bundle command, because there is react.gradle as part of the build step. If you look at the app build.gradle, you will see

The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
and bundleReleaseJsAndAssets).
These basically call react-native bundle with the correct arguments during the Android build
cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
bundle directly from the development server. Below you can see all the possible configurations
and their defaults. If you decide to add a configuration block, make sure to add it before the
apply from: “../../node_modules/react-native/react.gradle” line.

Nothing look suspicious here. I also tried clearing cache, closing debugger, deleting app but it still failed on Bitrise. The thing that it runs perfectly fine locally make me thing there’s mismatch between tooling and environments between local and Bitrise.

Matching tooling versions

Run react-native info to get versions of all the tools we use. Here I have

React Native Environment Info:
    System:
      OS: macOS High Sierra 10.13.6
      CPU: x64 Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
      Memory: 94.97 MB / 16.00 GB
      Shell: 5.3 - /bin/zsh
    Binaries:
      Node: 8.11.4 - ~/.nodenv/versions/8.11.4/bin/node
      npm: 5.6.0 - ~/.nodenv/versions/8.11.4/bin/npm
      Watchman: 4.9.0 - /usr/local/bin/watchman
    SDKs:
      iOS SDK:
        Platforms: iOS 11.4, macOS 10.13, tvOS 11.4, watchOS 4.3
    IDEs:
      Android Studio: 3.1 AI-173.4907809
      Xcode: 9.4.1/9F2000 - /usr/bin/xcodebuild
    npmPackages:
      react: 16.4.1 => 16.4.1
      react-native: ^0.56.0 => 0.56.0
    npmGlobalPackages:
      react-native-cli: 2.0.1
Enter fullscreen mode Exit fullscreen mode

So let’s make sure that we have the same node, npm, react-native versions. Luckily Bitrise has custom steps to change

But even with same versions and configurations, it still fails on Bitrise 😕

Trying with a clean git clone

The next thing that I should do is to delete the local repo and clone from remote again. And surprise, I actually get the same errors on Bitrise. It’s time to dig into the problems.
How to debug your build locally / "It works on my Mac/PC but not on bitrise.io"
*If the build works on your local Mac/PC but not on bitrise.io, that's usually because of uncommitted / gitignored files…*discuss.bitrise.io

Remember the workarounds I did to make react-native-camera works? To keep those changes, I track node_modules and Pods to git, as I don’t trust the npm system and also I want to see the changes to any of my dependencies. This way the CI can just build my apps without npm install or pod install

The thing is, with this, we have to understand .gitignore extremely well, as it is based on patterns. Some files are tracked, and some files are not, and it can gives room for inconsistence between local and CI. So the logical move is to ignore node_modules and Pods and somehow still keep the workaround changes. Tracking dependencies are useless as when it changes, there’s bunch of changes in git diff and we probably don’t know (and don’t want to) what’s happening.

The lesson that I learn is to never track generated or downloadable stuff, like node_modules, jsbundle, Pods as they are not our concern.

We have package-lock.json and Podfile.lock so we should be on the same versions. The package manager should handle the rest. Yeah, hopefully.

So, how to keep the workaround changes? I see there are several ways

Fork modules

React Native has a bunch of dependencies, and any of them can have bugs, especially during version change, for example this schedule 0.5.0 in React Native 0.57.0 . We can ope issues, wait, pray and hope that someone will come and fix. That is where we become dependants, we have no control over the things that we do. One way is to maintain your our own fork with the changes we need

Here is the proper way to do this while using npm to manage your forked version of the module:

  • Fork the project on GitHub
  • Clone the fork to your machine
  • Fix the bug or add the feature you want
  • Push your commits up to your fork on GitHub
  • Open your fork on GitHub, and click on the latest commit you made
  • On the page of that commit, click on the “Downloads” button
  • Right click on the “Download .tar.gz” button inside the popup, and copy the link (“Copy Link Address” in Chrome)
  • Open up your package.json file, and replace the version number of the module with the url you just copied
  • Send a pull request upstream (Optional, but this way you will avoid having to maintain that patch of yours against newer versions of the module you forked)

Or if we want to say goodbye to the old fashioned forks, we can use patch-package to apply patches
Say goodbye to old-fashioned forks thanks to the patch-package.
*Have you ever been in a situation when you had to fork the library to apply some fix to your app? While working with…*blog.callstack.io

Track inside ignored folders

Another way is to track just the files that we change. The .gitignore file generated by react-native init contains node_modules , which means that React Native team recommend not tracking node modules. But from git 2.8 , we can track files inside ignore folders. Here is how I keep my changes to RNCamera.xcodeproj inside node_modules . Use ! in .gitignore

!node_modules/react-native-camera/ios/RNCamera.xcodeproj
Enter fullscreen mode Exit fullscreen mode

This is fine, but too manual for a lazy programmer. Furthermore, it can be easily overwritten by the next npm install or pod install

Shell scripting

Lazy programmers should automate boring tasks. Did you know that npm can run lot of scripts in many events.
scripts | npm Documentation
*The place where all things npm are documented*docs.npmjs.com

npm supports the “scripts” property of the package.json file, for the following script

  • preinstall: Run BEFORE the package is installed
  • install, postinstall: Run AFTER the package is installed.
  • preuninstall, uninstall: Run BEFORE the package is uninstalled.
  • postuninstall: Run AFTER the package is uninstalled.

We can use shell script. For example to change to name in Pods/React/package.json to avoid the issue Duplicate module name: react-native, we can create a file call postinstall.sh , the name is arbitrary

#!/bin/bash
set -e

# Fix Duplicate module name: react-native

PODS_REACT_PACKAGE_JSON = 'ios/Pods/React/package.json'

if [ -e PODS_REACT_PACKAGE_JSON ]
then
    sed -i -e 's/"name": "react-native"/"name": "react-native-avoid-collision"/g' PODS_REACT_PACKAGE_JSON
fi

# Fix react-native-camera Argument list too long

REACT_NATIVE_CAMERA_PROJECT = ./node_modules/react-native-camera/ios/RNCamera.xcodeproj/project.pbxproj

if [ -e PODS_REACT_PACKAGE_JSON ]
then
    sed -i -e 's/ios\/\*\*/ios\/\*/g' REACT_NATIVE_CAMERA_PROJECT
fi
Enter fullscreen mode Exit fullscreen mode

And add postinstall inside scripts in the root package.json

"postinstall": "./postinstall.sh"
Enter fullscreen mode Exit fullscreen mode

Node scripting

There’s lot of code with shell scripting. We have node environment, let’s utilise that together with lots of packages like fs , so create a new file called postinstall.js . We can create function to encapsulate common functionalities.

const fs = require('fs')

function replaceJson(path, process) {
    if (fs.existsSync(path)) {
        let json = JSON.parse(fs.readFileSync(path, 'utf8'))
        json = process(json)
        fs.writeFileSync(path, JSON.stringify(json, null, 2))
    }
}

function replaceString(path, oldString, newString) {
    if (fs.existsSync(path)) {
        let string = fs.readFileSync(path, 'utf8')
        string = string.replace(oldString, newString)

fs.writeFileSync(path, string, 'utf8')
    }
}

function write(path, string) {
    fs.writeFileSync(path, string, 'utf8')
}

// Fix Duplicate module name: react-native
PODS_REACT_PACKAGE_JSON='ios/Pods/React/package.json'
replaceJson(PODS_REACT_PACKAGE_JSON, function(json) {
    json.name = 'react-native-avoid-collision'
    return json
})

// Fix react-native-camera Argument list too long
REACT_NATIVE_CAMERA_PROJECT='./node_modules/react-native-camera/ios/RNCamera.xcodeproj/project.pbxproj'
replaceString(REACT_NATIVE_CAMERA_PROJECT, 
    '$(SRCROOT)/../../ios/**', 
    '$(SRCROOT)/../../ios/'
)

replaceString(REACT_NATIVE_CAMERA_PROJECT, 
    '$(SRCROOT)/../../../ios/**', 
    '$(SRCROOT)/../../../ios/'
)

// Android sdk
const sdkDir = "sdk.dir = /Users/khoa/Library/Android/sdk"
LOCAL_PROPERTIES = "android/local.properties"
write(LOCAL_PROPERTIES, sdkDir)
Enter fullscreen mode Exit fullscreen mode

Now every time after we run npm install , the workaround we need has been reapplied for us. This helps in other situations like the issue

Error: SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.
Enter fullscreen mode Exit fullscreen mode

To solve this, we need to create local.properties with the SDK path. Mine is sdk.dir = /Users/khoa/Library/Android/sdk . This has my username in the path, which is very different than the username vagrant in Bitrise. I can’t seem to use ${HOME} or $HOME variable, so this change should not be tracked into git. You can see above that the script auto generates this file for us. So nifty.

Now everything works fine, and the build works fine too in Bitrise.

Deploy to Fabric with Crashlytics reporting

To be honest, I almost ended up building locally and uploading the IPA/APK to Fabric since I couldn’t find a way to solve building errors on Bitrise. But right now we have to go with Fabric any way, as it has good installation page for testers, and also its Crashlytics reporting is so good. The good (or annoying) thing about Fabric is that we need to successfully build and run an app before it is registered into Fabric.

For iOS, we need to install the Fabric macOS app and follow the instruction. I prefer dragging Fabric and Crashlytics frameworks into our Xcode projects more than using CocoaPods. Further more, if you archive project in Xcode, then you can manually choose archive and upload to Fabric. Simply choose the Archive tab in Fabric app.

For Android, we need to install Fabric for Android Studio. After installing, it is available in View -> Tool Widows -> Fabric

The tool only sets up simple things. We still need to configure gradle, follow the instructions on Install Crashlytics via Gradle

Deploy to CircleCI 2.0

CircleCI has a lot of configurations, and it takes time to learn. Here is a simple configuration to check if the React Native project can build well for both iOS and Android. Create a file called config.yml inside .circleci folder.

version: 2
jobs:
node:
docker:
- image: circleci/node:8
steps:
- checkout
- run: npm install

android:
docker:
- image: circleci/android:api-27-node8-alpha
steps:
- checkout
- run: npm install
- run: cd android && ./gradlew assembleRelease

ios:
macos:
xcode: "9.4.1"

use a --login shell so our "set Ruby version" command gets picked up for later steps

shell: /bin/bash --login -o pipefail
Enter fullscreen mode Exit fullscreen mode

steps:
- checkout
- run: npm install
- run: xcodebuild -project ios/MyApp.xcodeproj -scheme "MyApp" -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=11.4,name=iPhone X'

workflows:
version: 2
node-android-ios:
jobs:
- node
- android:
requires:
- node
- ios:
requires:
- node

Enter fullscreen mode Exit fullscreen mode




React Native in Xcode 10

For now with React Native 0.57.0 , it does not fully work in Xcode 10 yet. I often get this error error: Multiple commands produce . In that case we need to append -UseModernBuildSystem=N to xcodebuild command. Or in Xcode, we need to use the old legacy build system. Go to File -> Project Setting and set Build System to Legacy Build System

Where to go from here

In this post, we go through implementing QR code scanner feature with react-native-camera , how to debug issues with inconsistent between CI and local environment, how to clone repository to start from fresh, and how to deploy to Bitrise, Fabric and CircleCI. The key lesson for me is to track problem one after another, and never track generated or downloadable content in git, let the package manager handle that. We should also use more scripting to automate boring tasks and utilise node packages in scripting.

Original post https://medium.com/react-native-training/fixing-react-native-issues-and-happy-deploy-to-bitrise-fabric-circleci-44da4ab1487b

Discussion (0)