For creating a new package from scratch we can simple run a few commands to start having an skeleton of what is going to be our future package.
npx create-react-native-library@latest <package-name> --local
Official documentation about create-react-native-library
can be found here.
Notice the --local
flag that is used to create this skeleton. Without this the library will introduce also a React Native app as example when generating the skeleton. Since are not interested on it we have to make sure this command is ran using this flag.
After running this, the new skeleton should look like the following:
packages/
└── <package-name>/
├── ios/ # containing an Xcode workspace/project
├── android/ # containing an Android workspace/project
├── src/
│ ├── __tests__/
│ └── index.ts
├── tsconfig.json
├── package.json
├── .eslintrc.js
├── README.md
└── RNEmbrace<PackageName>.podspec # `RNEmbrace` is the prefix as per convention. Make sure the `.podspec` file generated by `create-react-native-library` is renamed following this convention. Also make sure to update the content of this file to rename the value of `s.name`.
Make sure the new
package.json
file list all files/folders we want to get packed. Notice that in the following example android has a very detailed list. That's the minimum the Android Native module needs for building/running into a React Native application.
{
"files": [
"lib",
"android/src",
"android/build.gradle",
"android/dependencies.gradle",
"android/gradle.properties",
"ios",
"RNEmbrace<PackageName>.podspec"
],
}
This example is configured to pack lib/, android/, ios/ and RNEmbrace.podspec into the package we will publish in the future. All folders/files that should be packed and published should be listed here. If they are not there, the pack/publish process will ignore them.
Notice that this command will generate a basic structure for each platform, but we also need to tweak certain file names and contents to properly customize the new package following the Embrace conventions.
References are very important when developing a new iOS Standalone Native Module, we encourage developers to create new modules through Xcode to avoid any issue related to this.
- Open Xcode and create a project: File -> New -> Project -> Static Library (for src files) into packages/PACKAGE_NAME/ios. This will create the RNEmbrace__NAME__.xcodeproj file and its respective configuration.
- Create a new target with the same name.
- Proper source files are supposed to be created by the
create-react-native-library
library following the iOS Native Modules for React Native official documentation. We are supposed to link them into the new project.
This repository already contains classes using Swift. We highly recommend to keep this approach. More information about how to do it can be found in the official documentation.
At the end of this process the ios folder structure should contain something like the following:
ios/
└── RNEmbrace<PackageName>/
├── RNEmbrace<PackageName>.xcodeproj
├── RNEmbrace<PackageName>-Bridging-Header.h
├── RNEmbrace<PackageName>.m
└── RNEmbrace<PackageName>.swift
This is the bare minimum we need to create a new iOS Native Module.
The proper
.podspec
file outside the ios folder listing all dependencies should be already created previously by running the command fromcreate-react-native-library
. It's a good idea to check that this file is in place after running the build and pack the new package (without this file the new iOS Native Module won't be recognized by the application and won't be installed).
- Under
packages/new-package
create atest-project
directory.
test-project /
└── android
└── ios
└── Podfile // This should contain the proper React Native install script adding all dependencies we need
└── package.json
└── yarn.lock
- Using XCode create a new Framework: Project -> File -> New -> Project -> Framework (iOS). a. Product Name -> RNEmbrace__NAME__ (no suffix Test here, should be the same name as the ios source package) b. Language -> Swift c. Team -> None d. Organization Identifier -> io.embrace.rnembrace__NAME__ e. Testing System -> XCTest
- Save it into /test-project/ios (this will create a dir called
RNEmbrace__NAME__
, aRNEmbrace__NAME__.xcodeproj
and a dirRNEmbrace__NAME__Tests
). - Remove the
/test-project/ios/RNEmbrace__NAME__/RNEmbrace__NAME__
dir, it's not needed (but keep the target). - Add a reference to the iOS source package in
RNEmbrace__NAME__
root folder (project). Do not copy/move files. The reference is what we need here (Location:Relative to a Group
) a. Action -> Reference files in place b. Groups -> Create Groups c. Targets -> both (regular and test targets) - Make sure to click into the new referenced folder, open the File Inspector (top right corner of XCode) and update the location of the folder to be
relative
to the project (instead ofabsolute
). It should show a relative path like../../../ios/RNEmbrace__NAME__
- After all make sure to follow steps in (this comment)[CocoaPods/CocoaPods#12583 (comment)]
- Move everything using XCode (because of references) from
packages/__PACKAGE_NAME__/test-project/ios/RNEmbrace__NAME__/
topackages/__PACKAGE_NAME__/test-project/ios/
(we don't need to keep the RNEmbrace__NAME__ dir). - At this point the structure will be ios/.xcodeproj/ + ios/REmbraceTracerProviderTests/
- Into the
.xcodeproj
dir runpod init
to initialize the Podfile with the minimum targets configuration (this will create the ios/Podfile) - Add all required dependencies + React Native (this can be copied/pasted from already existent suites)
- Run
pod install
to install the dependencies + create theRNEmbrace__NAME__.xcworkspace
(do not create it manually since it is going to require extra setup we don't want to go over). - Run
yarn install:ios
after creating the proper script in packages//package.json (take a look at other packages as references). - This should install all dependencies related to React Native and those coming from Embrace (listed in
ios/Podfile
).
-
Make sure it is created the
RNEmbrace__NAME__Tests.xctestplan
intotest-project/ios
by opening Edit Scheme (top center of xcode) -> look for Tests Plan -> Click on the arrow at the right of the name. If it is the first time you click on it and theRNEmbrace__NAME__Tests.xctestplan
file was not saved yet, it will ask for it. Save it, so the changes will persist. Otherwise the configurations will be lost. -
Make sure swift classes add the
React
import at the top of the file. -
Make sure
BUILD_LIBRARY_FOR_DISTRIBUTION / Build Libraries for Distribution
is set toNo
for regular target (not the test one). -
Make sure
ENABLE_USER_SCRIPT_SANDBOXING / User Script Sandboxing
is set toNO
for regular target (not the test one). -
Make sure to run
rm .xcode.env.local
is part of the ios:install script on each package (more info about it can be found (here)[facebook/react-native#36762 (comment)]). -
At this point the structure should look similarly to
test-project/
└── ios
├── RNEmbrace<PackageName>/ (just the reference)
├── RNEmbrace<PackageName>Tests/
│ └── RNEmbrace<PackageName>Tests.swift
├── RNEmbrace<PackageName>Tests.xcworkspace
├── RNEmbrace<PackageName>Tests.xcodeproj // created manually
└── Podfile
└── package.json
└── yarn.lock
- Notice that
RNEmbrace<PackageName>
andRNEmbrace<PackageName>Tests
are the Framework + XCTest targets created by xcode.
Tests can be run from XCode by opening test-project/ios/RNEmbraceTests.xcworkspace as a Workspace running the Test target or from the CLI with yarn ios:test
- Using Android Studio for creating a new project: File -> New -> New Project -> Empty Activity + kotlin (into
package/<package-name>test-project/android
dir). Make sure there is support for JUnit tests. - Rename the
settings.gradle.kts
-> to justsettings.gradle
(just for consistency, other packages has this file already written in groovy). - Add React Native configuration (this can be copied/pasted from other existent packages like
@embrace-io/react-native-tracer-provider
) - Make sure includeBuild("../node_modules/@react-native/gradle-plugin") is added into
pluginManagement
- Include the local package we want to test (
include ':react-native-<package-name>'
) - Link the local package into test (
project(':react-native-tracer-provider').projectDir = file('../../android')
) - Update rootProject.name (
rootProject.name = "io.embrace.rnembrace<packagename>test"
) - Into the
gradle.properties
-> Add custom properties (particularlyRNEmbrace<PackageName>_packageJsonPath
following other packages as example) - Add
android/config
folder with respective content (detekt
plugin for linting, in the future this is going to be moved at the root of the repo avoiding config duplication)
- Remove app/src/androidTest
- Remove app/src/main/java
- Remove all app/src/main/res except for app/src/main/res/values/strings.xml and app/src/main/AndroidManifest.xml
- Rename app/src/test/java/io/embrace/xxxx/ExampleUnitTest.kt to
RNEmbrace__NAME__Test
.kt - Replace content of app/build.gradle.kts with proper configuration (using as example what other packages)
- Make sure
app/build.gradle.kts
includes the right local package (implemented insettings.gradle
) - Make sure
apply("../../../android/dependencies.gradle")
is added intoapp/build.gradle.kts
- Make sure
android/gradle/wrapper/gradle-wrapper.properties
points to a gradle version we support (7.5.1 atm) - Make sure
android/build.gradle
includes an AGP we support (com.android.tools.build:gradle:7.4.2 atm)
Tests can be run from Android Studio by adding test-project/android as a Project or from the CLI with yarn android:test
.
If there are calls to NativeModules.<YourModule>.<method>
that aren't explicitly covered by error handling then a
wrapper module should be defined to give a better error message in the case that the package hasn't been installed
correctly on the native side:
// packages/your-package/src/YourModule.ts
import {NativeModules, Platform} from "react-native";
const LINKING_ERROR =
`The package '@embrace-io/your-package' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ios: "- You have run 'pod install'\n", default: ""}) +
"- You rebuilt the app after installing the package\n" +
"- You are not using Expo Go\n";
export const YourModule = NativeModules.NativeModuleName
? NativeModules.NativeModuleName
: new Proxy(
{},
{
get() {
throw new Error(LINKING_ERROR);
},
},
);