How to set up a Monorepo with React Native You.I and Yarn Workspaces

Andrei Calazans
g2i_co
Published in
8 min readFeb 6, 2020

--

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.jsfile:


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.jsfile:

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:

Folder Hierarchy

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.

App One Running in Monorepo Project

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

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 have react as a dependency.
  • With Yarn Workspaces your shared packages should not have react-native and @youi/react-native-youi as dependencies, this can cause collision conflicts.

--

--