If you need an introduction to Yarn Workspaces: Yarn Blog
If you prefer looking at the finished repository: GitHub
Our goal for this blog post is to have a basic monorepo setup that contains one bare React Native app and one Next.js project. This will result in a file structure like this:
tree monorepo-tutorial
monorepo-tutorial
├── package.json
└── packages
├── app
└── web
For starters we create our root directory and initialize a fresh project with git repository.
mkdir monorepo-tutorial && cd monorepo-tutorial && yarn init -y && echo node_modules > .gitignore && git init
Since both of our packages will depend on react
we will lift up the dependency to the root level of our monorepo. Note that we also add react-dom
in case we want to create more web packages later.
yarn add -W react react-dom
In our package.json
we define a workspace structure. The below glob defined in workspaces
tells Yarn where our monorepo packages are located.
{
+ "private": true,
+ "name": "root",
"version": "1.0.0",
"main": "index.js",
"author": "ecklf",
"license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ]
}
We can now proceed with creating our packages folder.
mkdir packages && cd packages
Let's start by initializing a fresh React Native project from the template:
npx react-native init app --template react-native-template-typescript
You should now encouter this error:
Failed to install CocoaPods dependencies for iOS project.
This is perfectly fine since the template's CocoaPods configuration has the wrong path to react-native
.
Continue by removing the react
dependency from the template since we will resolve it from the root level.
cd app && yarn remove react
From my experience Metro plays the nicest in monorepos when launched separately with yarn start
, so we disable the packaging when running ios
/ android
scripts. While we are at it we can also update the package name.
{
+ "private": true,
+ "name": "@monorepo/app",
"version": "1.0.0",
"main": "index.js",
"author": "ecklf",
"license": "MIT",
"scripts": {
- "android": "react-native run-android",
+ "android": "react-native run-android --no-packager",
- "ios": "react-native run-ios",
+ "ios": "react-native run-ios --no-packager",
},
}
Create the file react-native.config.js
with the following content:
+ module.exports = {
+ reactNativePath: '../../node_modules/react-native',
+ };
Update metro.config.js
to have an additional watch folder at root level.
+ const path = require('path');
module.exports = {
+ watchFolders: [path.resolve(__dirname, '../../')],
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};
We need to add aliases to explicitly define where our root-level packages are located.
yarn add -D @babel/runtime babel-plugin-module-resolver
const path = require("path");
module.exports = {
presets: ["module:metro-react-native-babel-preset"],
plugins: [
[
"module-resolver",
{
root: ["./src"],
alias: {
react: require.resolve("react", {
paths: [path.join(__dirname, "./")],
}),
"^react-native$": require.resolve("react-native", {
paths: [path.join(__dirname, "./")],
}),
"^react-native/(.+)": ([, name]) =>
require.resolve(`react-native/${name}`, {
paths: [path.join(__dirname, "./")],
}),
},
extensions: [
".ios.js",
".ios.ts",
".ios.tsx",
".android.js",
".android.ts",
".android.tsx",
".native.js",
".native.ts",
".native.tsx",
".js",
".ts",
".tsx",
],
},
],
],
};
Podfile
First, we fix our previous install error by now pointing to our root's node_modules
folder.
- require_relative '../node_modules/react-native/scripts/react_native_pods'
+ require_relative '../../../node_modules/react-native/scripts/react_native_pods'
- require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
+ require_relative '../../../node_modules/@react-native-community/cli-platform-ios/native_modules'
We can confirm if this worked by installing our pods:
npx pod install
Xcode (workspace) - Signing & Capabilities
Add your development team to build the project.
Xcode (workspace) - Build Phases
Nothing special here. We just adjust the paths like in CocoaPods.
Start Packager
- echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${SRCROOT}/../node_modules/react-native/scripts/.packager.env"
+ echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${SRCROOT}/../../../node_modules/react-native/scripts/.packager.env"
- open "$SRCROOT/../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
+ open "$SRCROOT/../../../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
Xcode (workspace) - Bundle React Native code and images
- ../node_modules/react-native/scripts/react-native-xcode.sh
+ ../../../node_modules/react-native/scripts/react-native-xcode.sh
Build Settings - User-Defined
Add a user-defined setting (+ sign at the top menu bar) RCT_NO_LAUNCH_PACKAGER
with the value 1
.
Getting things to work on Android is just a matter of adding paths for hermes
+ react-native
cli and updating the existing ones.
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
- url("$rootDir/../node_modules/react-native/android")
+ url("$rootDir/../../../node_modules/react-native/android")
}
maven {
// Android JSC is installed from npm
- url("$rootDir/../node_modules/jsc-android/dist")
+ url("$rootDir/../../../node_modules/jsc-android/dist")
}
- apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
+ apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
project.ext.react = [
- enableHermes: false, // clean and rebuild if changing
+ enableHermes: true, // clean and rebuild if changing
+ hermesCommand: "../../../../node_modules/hermes-engine/%OS-BIN%/hermesc",
+ composeSourceMapsPath: "../../node_modules/react-native/scripts/compose-source-maps.js",
+ cliPath: "../../node_modules/react-native/cli.js"
]
- apply from: "../../node_modules/react-native/react.gradle"
+ apply from: "../../node_modules/react-native/react.gradle"
- def hermesPath = "../../node_modules/hermes-engine/android/";
+ def hermesPath = "../../../../node_modules/hermes-engine/android/";
- apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
+ apply from: file("../../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
yarn start
yarn ios
yarn android
Fortunately adding a Next.js project is more straightforward. All we need to do is delete package-lock.json
(we use yarn not npm) and remove our root dependencies from the template.
npx create-next-app@latest --ts web
rm package-lock.json && yarn remove react react-dom
{
+ "private": true,
+ "name": "@monorepo/web",
+ "version": "1.0.0",
"main": "index.js",
"author": "ecklf",
"license": "MIT",
}