How to set up a Monorepo with React Native You.I and Yarn Workspaces
In order to simplify dependency management, code reuse, and collaboration across teams, many projects have sought out to use the Monorepo structure. This post will outline how to achieve a Monorepo folder structure using React Native You.I alongside Yarn Workspaces.
Things this post covers:
- How Yarn Workspaces work.
- The errors Metro will throw.
- Jest Testing.
- A Possible Alternative.
Goal
Our goal is to have two apps in the same repository, and allow these two apps to use the same modules inside a directory named shared
. We also want to be able to run tests separately for each app.
- apps
- app_one
- app_two
- shared
- module_one
- module_two
You can go ahead and try to do this yourself, we will generate the apps using youi-tv cli
, however, if you are not using React Native You.I, you can use the react-native-cli
instead.
To achieve this goal we will:
Create The Repository
In the location you want to create the project, run the following commands in your terminal line to create the folders, initialize git, initialize yarn, and generate the apps.
- Create Folders
mkdir monorepo
cd monorepo && git init
yarn init
mkdir apps
mkdir shared
- Generate Apps
cd apps && youi-tv init app_one
cd apps && youi-tv init app_two
- Create Shared Modules
cd shared && mkdir module_one && cd module_one && yarn init
Paste the following in your terminal to create the index.js
file:
echo "import React from ‘react’;
import { Text } from ‘react-native’;
import { FormFactor } from ‘@youi/react-native-youi’;export function moduleOne() {
return <Text>{FormFactor.isTV ? “Module one for TV” : “Module one for others”}</Text>;
}
" >> shared/module_one/index.js
cd shared && mkdir module_two && cd module_two && yarn init
Paste the following in your terminal to create the index.js
file:
echo "export function moduleTwo() {
return "Module Two";
}
" >> shared/module_two/index.js
If you are following along, you should have the following structure by now:
Set Up Yarn Workspaces
According to the documentation we have to add the following configuration in our package.json file:
“workspaces”: {
“packages”: [
“shared/*”,
“apps/app_one”,
“apps/app_two”
]
},
This will allow our apps to access the shared packages.
Fixing Yarn Workspaces
Once you have added the configuration, you will have to delete all of your node_modules and install them again.
Run the following command in your terminal line to find all nested node_modules, delete them, and run yarn again:
find . -type dir -name node_modules | xargs rm -rf && yarn
Now that this is done, technically we should be able to build and start our app.
If you remember the youi-tv cli
output:
Run `youi-tv docs` to see next steps.Or run the following (on platform OSX for example):cd app_twoyoui-tv build -p osx./youi/build/osx/Debug/app_two
We should be able to do that and run the app. Let's try?
In app_one
build an osx
app by running: youi-tv build -p osx
inside apps/app_one
Then, in that same directory, run yarn start
You will see the following error:
~/youi/monorepo/apps/app_one master yarn start
yarn run v1.19.1
$ node node_modules/react-native/local-cli/cli.js start
internal/modules/cjs/loader.js:895
throw err;
^Error: Cannot find module ‘/Users/andrei/youi/monorepo/apps/app_one/node_modules/react-native/local-cli/cli.js’
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:892:15)
at Function.Module._load (internal/modules/cjs/loader.js:785:27)
at Function.Module.runMain (internal/modules/cjs/loader.js:1143:12)
at internal/main/run_main_module.js:16:11 {
code: ‘MODULE_NOT_FOUND’,
requireStack: []
}
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
What is happening?
If you look inside <root>/apps/app_one/package.json
the start
script tries to call cli.js
with a relative path. And since we are using workspaces, all of our dependencies were lifted up to the root of the project, therefore, they are no longer inside <root>/apps/app_one/node_modules
, they are instead in <root>/node_modules
.
We can fix this by using the symlink of React Native
provided by yarn
Replace both start
and react-native
scripts with the following:
“start”: “react-native start”,
“react-native”: “react-native”,
Note, the react-native script here is required because when we bundle the JavaScript code, our Cmake config runs yarn react-native bundle to achieve this. If you are using just React Native omit that script.
Does yarn start work now?
Yes, it should. You can run yarn start
to test it. Now since you generated the osx
app, you can run it directly from the terminal line with the following command inside the app_one
directory:
./youi/build/osx/debug/app_one
You will notice it attaches to the Metro packager, however, it fails with a similar message to this:
Loading dependency graph, done. Error: Unable to resolve module `./index.youi` from `/Users/andrei/youi/monorepo/.`: The module `./index.youi` could not be found from `/Users/andrei/youi/monorepo/.`. Indeed, none of these files exist: * `/Users/andrei/youi/monorepo/index.youi(.native||.youi.js|.native.js|.js|.youi.json|.native.json|.json|.youi.ts|.native.ts|.ts|.youi.tsx|.native.tsx|.tsx)` * `/Users/andrei/youi/monorepo/index.youi/index(.native||.youi.js|.native.js|.js|.youi.json|.native.json|.json|.youi.ts|.native.ts|.ts|.youi.tsx|.native.tsx|.tsx)`
It can’t find your index.youi
in /Users/andrei/youi/monorepo/
This expected since index.youi.js
is in app_one
To solve this we have to extend Metro’s config to also add this new root which is the apps/app_one
.
Replace rn-cli.config.js
in app_one
for the following metro.config.js
Note rn-cli was deprecated in favor of metro.config
const blacklist = require('metro-config/src/defaults/blacklist')
const path = require('path');module.exports = {
// Add additional folders to watch that are outside app's root
watchFolders: [path.resolve(__dirname, '../../node_modules'), path.resolve(__dirname, '../../shared')],
// Black list build folders to not resolve any JavaScript files in there.
resolver: {
blacklistRE: blacklist([/\/youi\/build\/.*/])
}
};
By putting this metro.config.js
in app_one
Metro understands the new root
of the project.
Problems with hoisting
Once you have overcome the issues with the root of the project. You can try running yarn start
again and connect your app. You will run into the following problem:
Loading dependency graph, done. error: bundling failed: Error: Unable to resolve module Composition from /Users/andrei/youi/monorepo/node_modules/@youi/react-native-youi/Libraries/react-native-youi/react-native-youi-implementation.js: Module Composition does not exist in the Haste module map
or
Unable to resolve module `AccessibilityInfo` from `/Users/andrei/youi/monorepo/node_modules/react-native/Libraries/react-native/react-native-implementation.js`: Module `AccessibilityInfo` does not exist in the Haste module map
The following error is related to how the workspace lifts up your dependencies to the root of your project. We can disable this feature per module by changing the configuration of the workspace as follows:
"workspaces": {
"nohoist": [
"**react-native**"
],
"packages": [
"shared/*",
"apps/app_one",
"apps/app_two"
]
},
Note — Adding "**react-native**" includes both react-native and the react-native-youi folder to the no hoist rule.
A Working App
And now you should be able to run your app and see it connect correctly.
Jest Testing
The last piece of the puzzle is to make sure the tests work. Let's create the following test inside app_one
You can create a file in <root>/apps/app_one/app.test.js
with the following test:
import React from 'react';
import { View } from 'react-native';
import { FormFactor } from '@youi/react-native-youi';describe('App', () => {
it('should work', () => {
expect(true).toBe(true);
})
it('should have react-native modules', () => {
expect(View).toBeTruthy();
})it('should have @youi/rect-native-youi modules', () => {
expect(FormFactor).toBeTruthy();
})
})
Then inside app_one
folder you can run:
yarn test
Output:
✘ ~/youi/monorepo/apps/app_one master yarn test yarn run v1.19.1
$ jest [ ‘/Users/andrei/youi/monorepo/apps/app_one/node_modules/react-native/’, ‘/Users/andrei/youi/monorepo/apps/app_one/node_modules/@youi/react-native-youi/’ ] FAIL ./app.test.js ● Test suite failed to run /Users/andrei/youi/monorepo/apps/app_one/node_modules/@youi/react-native-youi/jest/setup.js:328 import { WebSocket } from ‘mock-socket’; ^^^^^^ SyntaxError: Cannot use import statement outside a module at ScriptTransformer._transformAndBuildScript (../../node_modules/@jest/transform/build/ScriptTransformer.js:537:17) at ScriptTransformer.transform (../../node_modules/@jest/transform/build/ScriptTransformer.js:579:25) Test Suites: 1 failed, 1 total Tests: 0 total Snapshots: 0 total Time: 0.32s Ran all test suites. error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
This occurs because react-native
and @youi/react-native-youi
ship uncompiled ES6 code. For Jest to compile this code you must whitelist it in the transformIgnorePatterns
option in your package.json
as follows:
“transformIgnorePatterns”: [ “/node_modules/(?!react-native|@youi)” ],
After making this change, you can run yarn test
again.
You should see the following problem:
yarn workspace v1.19.1yarn run v1.19.1$ jest['/Users/andrei/youi/monorepo/apps/app_one/node_modules/react-native/','/Users/andrei/youi/monorepo/apps/app_one/node_modules/@youi/react-native-youi/']FAIL ./app.test.jsApp✕ should work (7ms)● App › should workConfiguration error:Could not locate module React mapped as:/Users/andrei/youi/monorepo/apps/app_one/node_modules/react.Please check your configuration for these entries:{"moduleNameMapper": {"/^React$/": "/Users/andrei/youi/monorepo/apps/app_one/node_modules/react"},"resolver": null}at createNoMappedModuleFoundError (../../node_modules/jest-resolve/build/index.js:501:17)at Object.<anonymous> (node_modules/react-native/Libraries/Components/View/View.js:51:20)
It is now complaining of a the moduleNameMapper
configuration set by our @youi/react-native-youi
preset.
There are two ways to fix this issue. One is overriding the preset configuration by making a custom preset and either pasting the preset in @youi/react-native-youi
or spreading it over our JSON object. The second is also not hoisting the React
package. We will choose the second option since that has been our way to go with the other modules as well.
Add React
to the nohoist
option:
"workspaces": {
"nohoist": [
"**react**",
"**react-native**"
],
"packages": [
"shared/*",
"apps/app_one",
"apps/app_two"
],
},
Result of yarn test
Alternative
As you can see the usage of Yarn Workspaces
has proven to be quite burdensome to maintain. Overall, as your codebase progress and new updates occur, you might need to update and fix this Monorepo
structure. To avoid having this kind of issue, and to also enjoy similar benefits. You could instead structure your Monorepo
without symlinks created by yarn
.
How could we structure a Monorepo
without Yarn Workspaces
?
Under the same repository, you could have multiple independent projects linked by installing it via a relative path.
- apps
- app_one
- app_two
- shared
- module_one
- module_two
app_one
and app_two
can consume the shared
module by installing it as follow:
yarn add file:../../shared/module_one
This will add the following to your package.json
”module_one”: “file:../../shared/module_one”,
Note, To avoid reinstalling the same dependencies make sure to declare modules as peerDependencies
inside your shared modules if you also use them.
This approach has the benefit of being compatible with NPM as well.
Possible Problems
If your shared modules have external dependencies and for some reason, you install them by running yarn
inside shared/module
, you have to always remember to remove the node_modules
folder before running your app, else you will have name collisions.
Important Notes
- With
Yarn Workspaces
your shared packages will need to havereact
as a dependency. - With
Yarn Workspaces
your shared packages should not havereact-native
and@youi/react-native-youi
as dependencies, this can cause collision conflicts.