xref: /expo/docs/pages/guides/monorepos.mdx (revision 75d433ca)
1---
2title: Work with monorepos
3description: Learn about setting up Expo projects in a monorepo with Yarn v1 workspaces.
4---
5
6import { Terminal } from '~/ui/components/Snippet';
7import { Collapsible } from '~/ui/components/Collapsible';
8
9Monorepos, or _"monolithic repositories"_, are single repositories containing multiple apps or packages. It can help speed up development for larger projects, make it easier to share code, and act as a single source of truth. This guide will set up a simple monorepo with an Expo project. We currently have first-class support for [Yarn 1 (Classic)](https://classic.yarnpkg.com/lang/en/) workspaces. If you want to use another tool, make sure you know how to configure it.
10
11> **warning** Monorepos are not for everyone. It requires in-depth knowledge of the used tooling, adds more complexity, and often requires specific tooling configuration. You can get far with just a single repository.
12
13## Example monorepo
14
15In this example, we will set up a monorepo using Yarn workspaces without the [nohoist](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/) option. We will assume some familiar names, but you can fully customize them. After this guide, our basic structure should look like this:
16
17- **apps/** - Contains multiple projects, including React Native apps.
18- **packages/** - Contains different packages used by our apps.
19- **package.json** - Root package file, containing Yarn workspaces config.
20
21### Root package file
22
23All Yarn monorepos should have a "root" **package.json** file. It is the main configuration for our monorepo and may contain packages installed for all projects in the repository. You can run `yarn init`, or create the **package.json** manually. It should look something like this:
24
25```json package.json
26{
27  "name": "monorepo",
28  "version": "1.0.0"
29}
30```
31
32### Set up yarn workspaces
33
34Yarn and other tooling have a concept called _"workspaces"_. Every package and app in our repository has its own workspace. Before we can use them, we have to instruct Yarn where to find these workspaces. We can do that by setting the `workspaces` property using [glob patterns](https://classic.yarnpkg.com/lang/en/docs/workspaces/#toc-tips-tricks), in the **package.json**:
35
36```json package.json
37{
38  "private": true,
39  "name": "monorepo",
40  "version": "1.0.0",
41  "workspaces": ["apps/*", "packages/*"]
42}
43```
44
45> **warning** Yarn workspaces require the root **package.json** to be private. If you don't set this, `yarn install` will error with a message mentioning this.
46
47### Create our first app
48
49Now that we have the basic monorepo structure set up, let's add our first app.
50
51Before we can create our app, we have to create the **apps/** folder. This folder can contain all separate apps or websites that belong to this monorepo. Inside this **apps/** folder, we can create a subfolder that contains the React Native app.
52
53<Terminal cmd={['$ yarn create expo-app apps/cool-app']} />
54
55> If you have an existing app, you can copy all those files inside a subfolder.
56
57After copying or creating the first app, run `yarn` to check for common warnings.
58
59#### Modify the Metro config
60
61Metro doesn't come with monorepo support by default (yet). That's why we need to configure Metro and tell it where to find certain things. There are three main changes we need to:
62
631. Make sure Metro is watching the full monorepo, not just **apps/cool-app**.
642. Tell Metro where it can resolve packages. They might be installed in **apps/cool-app/node_modules** or **node_modules**.
653. Force Metro to only resolve (sub)packages from the `nodeModulesPaths`.
66
67We can configure this by [creating a **metro.config.js**](/guides/customizing-metro/#customizing) with the following content:
68
69```js metro.config.js
70const { getDefaultConfig } = require('expo/metro-config');
71const path = require('path');
72
73// Find the project and workspace directories
74const projectRoot = __dirname;
75// This can be replaced with `find-yarn-workspace-root`
76const workspaceRoot = path.resolve(projectRoot, '../..');
77
78const config = getDefaultConfig(projectRoot);
79
80// 1. Watch all files within the monorepo
81config.watchFolders = [workspaceRoot];
82// 2. Let Metro know where to resolve packages and in what order
83config.resolver.nodeModulesPaths = [
84  path.resolve(projectRoot, 'node_modules'),
85  path.resolve(workspaceRoot, 'node_modules'),
86];
87// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
88config.resolver.disableHierarchicalLookup = true;
89
90module.exports = config;
91```
92
93> Learn more about [customizing Metro](/guides/customizing-metro).
94
95<Collapsible summary="1. Why do we need to watch all files with the monorepo?">
96
97Metro has three separate stages in its bundling process, [documented here](https://facebook.github.io/metro/docs/concepts). During the first phase, **Resolution**, Metro resolves your app's required files and dependencies. Metro does that with the `watchFolders` option, which is set to the project directory by default. This default setting works great for apps that don't use a monorepo structure.
98
99When using monorepos, your app dependencies splits up into different directories. Each of these directories must be within the scope of the [watchFolders](https://facebook.github.io/metro/docs/configuration/#watchfolders). If a changed file is outside of that scope, Metro won't be able to find it. Setting this path to the root of your monorepo will force Metro to watch all files within the repository and possibly cause a slow initial startup time.
100
101As your monorepo increases in size, watching all files within the monorepo becomes slower. You can speed things up by only watching the packages your app uses. Typically, these are the ones that are installed with an asterisk (\*) in your **package.json**. For example:
102
103```js
104const { getDefaultConfig } = require('expo/metro-config');
105const path = require('path');
106
107const projectRoot = __dirname;
108const workspaceRoot = path.resolve(projectRoot, '../..');
109
110const config = getDefaultConfig(workspaceRoot);
111
112// Only list the packages within your monorepo that your app uses. No need to add anything else.
113// If your monorepo tooling can give you the list of monorepo workspaces linked
114// in your app workspace, you can automate this list instead of hardcoding them.
115const monorepoPackages = {
116  '@acme/api': path.resolve(workspaceRoot, 'packages/api'),
117  '@acme/components': path.resolve(workspaceRoot, 'packages/components'),
118};
119
120// 1. Watch the local app folder, and only the shared packages (limiting the scope and speeding it up)
121// Note how we change this from `workspaceRoot` to `projectRoot`. This is part of the optimization!
122config.watchFolders = [projectRoot, ...Object.values(monorepoPackages)];
123
124// Add the monorepo workspaces as `extraNodeModules` to Metro.
125// If your monorepo tooling creates workspace symlinks in the `node_modules` folder,
126// you can either add symlink support to Metro or set the `extraNodeModules` to avoid the symlinks.
127// See: https://facebook.github.io/metro/docs/configuration/#extranodemodules
128config.resolver.extraNodeModules = monorepoPackages;
129
130// 2. Let Metro know where to resolve packages and in what order
131config.resolver.nodeModulesPaths = [
132  path.resolve(projectRoot, 'node_modules'),
133  path.resolve(workspaceRoot, 'node_modules'),
134];
135// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
136config.resolver.disableHierarchicalLookup = true;
137```
138
139</Collapsible>
140
141<Collapsible summary="2. Why do we need to tell Metro how to resolve packages?">
142
143This option is important to resolve libraries in the correct **node_modules** directories. Monorepo tooling, like Yarn, usually creates two different **node_modules** directories which are used for a single workspace.
144
1451. **apps/mobile/node_modules** - The "project" folder
1462. **node_modules** - The "root" folder
147
148Yarn uses the root folder to install packages used in multiple workspaces. If a workspace uses a different package version, it installs that different version in the project folder.
149
150We have to tell Metro to look in these two folders. The order is important here because the project folder **node_modules** can contain specific versions we use for our app. When the package does not exist in the project folder, it should try the shared root folder.
151
152</Collapsible>
153
154<Collapsible summary="3. Why do we need to disable the hierarchical lookup?">
155
156This option is important for certain edge cases, such as a monorepo that includes multiple versions of the `react` package. For example, let's say you have the following monorepo:
157
1581. **apps/marketing** - A simple Next.js website to attract new users. (uses `[email protected]`)
1592. **apps/mobile** - Your Expo app. (uses `[email protected]`)
1603. **apps/web** - Your Next.js website. (uses `[email protected]`)
161
162With monorepo tooling like Yarn, React is installed in two different **node_modules** folders.
163
1641. **node_modules** - The root folder, contains `[email protected]`.
1652. **apps/mobile/node_modules** - The app's folder, contains `[email protected]`.
166
167Expo modules and React Native libraries usually don't add `react` as a peer dependency. As a result, monorepo tooling, like Yarn, will install these dependencies to the root **node_modules** directory, for example:
168
1691. **node_modules** - The root folder, contains `expo@...` and `[email protected]`.
1702. **apps/mobile/node_modules** - The app's folder, contains `[email protected]`.
171
172With hierarchical lookup enabled, whenever `expo` imports `react`, Metro will resolve to `[email protected]` and not `[email protected]`. This causes "multiple React versions" errors in your app.
173
174By disabling hierarchical lookup, we can force Metro to resolve only folders from the `nodeModulesPaths = [...]` order we defined in #2.
175This option is documented in the [Metro Resolution Algorithm documentation](https://facebook.github.io/metro/docs/resolution/#algorithm), under step 5.
176
177When we disable this hierarchical lookup, it should not matter where the React Native library is installed.
178Whenever a library imports `react`, or any other library, Metro always resolves the library from the `nodeModulesPaths` we defined.
179As long as the **apps/mobile/node_modules** path has the correct library version and is listed as the first `nodeModulesPaths` entry, we should always get the correct version of that library.
180
181</Collapsible>
182
183#### Change default entrypoint
184
185In monorepos, we can't hardcode paths to packages anymore since we can't be sure if they are installed in the root **node_modules** or the workspace **node_modules** folder. If you are using a managed project, we have to change our default entrypoint to `node_modules/expo/AppEntry.js`.
186
187Open our app's **package.json**, change the `main` property to `index.js`, and create this new **index.js** file in the app directory with the content below.
188
189```js index.js
190import { registerRootComponent } from 'expo';
191
192import App from './App';
193
194// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
195// It also ensures that whether you load the app in Expo Go or in a native build,
196// the environment is set up appropriately
197registerRootComponent(App);
198```
199
200> This new entrypoint already exists for bare projects. You only need to add this if you have a managed project.
201
202If you are using [Expo Router](/routing/introduction/), define the [`EXPO_USE_METRO_WORKSPACE_ROOT`](/more/expo-cli/#environment-variables) environment variable when running the `npx expo start` command. It enables the auto server root detection for Metro.
203
204<Terminal cmd={['$ EXPO_USE_METRO_WORKSPACE_ROOT=1 npx expo start']} />
205
206This variable can also be defined inside a **.env** file.
207
208### Create a package
209
210Monorepos can help us group code in a single repository. That includes apps but also separate packages. They also don't need to be published. The [Expo repository](https://github.com/expo/expo) uses this as well. All the Expo SDK packages live inside the [**packages/**](https://github.com/expo/expo/tree/main/packages) folder in our repo. It helps us test the code inside one of our [**apps/**](https://github.com/expo/expo/tree/main/apps/native-component-list) before we publish them.
211
212Let's go back to the root and create the **packages/** folder. This folder can contain all the separate packages that you want to make. Once you are inside this folder, we need to add a new subfolder. The subfolder is a separate package that we can use inside our app. In the example below, we named it **cool-package**.
213
214<Terminal
215  cmd={[
216    '# Create our new package folder',
217    '$ mkdir -p packages/cool-package',
218    '$ cd packages/cool-package',
219    '',
220    '# And create the new package',
221    '$ yarn init',
222  ]}
223  cmdCopy="mkdir -p packages/cool-package && cd packages/cool-package && yarn init"
224/>
225
226We won't go into too much detail in creating a package. If you are not familiar with this, please consider using a simple app without monorepos. But, to make the example complete, let's add an **index.js** file with the following content:
227
228```js index.js
229export const greeting = 'Hello!';
230```
231
232### Using the package
233
234Like standard packages, we need to add our **cool-package** as a dependency to our **cool-app**. The main difference between a standard package, and one from the monorepo, is you'll always want to use the _"current state of the package"_ instead of a version. Let's add **cool-package** to our app by adding `"cool-package": "*"` to our app **package.json** file:
235
236```json package.json
237{
238  "name": "cool-app",
239  "version": "1.0.0",
240  "scripts": {
241    "start": "expo start",
242    "android": "expo start --android",
243    "ios": "expo start --ios",
244    "web": "expo start --web"
245  },
246  "dependencies": {
247    "cool-package": "*",
248    "expo": "~43.0.2",
249    "expo-status-bar": "~1.1.0",
250    "react": "17.0.1",
251    "react-dom": "17.0.1",
252    "react-native": "0.64.3",
253    "react-native-web": "0.17.1"
254  },
255  "devDependencies": {
256    "@babel/core": "^7.12.9"
257  }
258}
259```
260
261> After adding the package as a dependency, run `yarn install` to install or link the dependency to your app.
262
263Now you should be able to use the package inside your app! To test this, let's edit the **App.js** in our app and render the `greeting` text from our **cool-package**.
264
265```jsx App.js
266import { greeting } from 'cool-package';
267import { StatusBar } from 'expo-status-bar';
268import React from 'react';
269import { Text, View } from 'react-native';
270
271export default function App() {
272  return (
273    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
274      <Text>{greeting}</Text>
275      <StatusBar style="auto" />
276    </View>
277  );
278}
279```
280
281## Common issues
282
283As mentioned earlier, using monorepos is not for everyone. You take on increased complexity and need to solve issues you most likely will run into. Here are a couple of common issues you might encounter.
284
285### Can I use another monorepo tool instead of Yarn workspaces?
286
287There are a lot of monorepo tools available, and each of these tools has its benefits. It's hard for us to keep up with the latest tools and methods, and because of that, we can't officially support new monorepo tools. That being said, if the tool follows these three rules, it should work fine.
288
289<Collapsible summary="1. All dependencies must be installed in a node_modules directory">
290
291React Native dependencies contain many other files besides JavaScript, like Gradle files such as **react-native/react.gradle**. These native files are referenced from different sources other than Node.js, and because of that, it makes it fundamentally incompatible with concepts like Plug'n'Play modules.
292
293</Collapsible>
294
295<Collapsible summary="2. Dependencies used in multiple workspaces can be installed in the root node_modules directory">
296
297Whenever multiple workspaces use the same version of a single dependency, they can be installed in a root **node_modules** directory. Monorepo tools usually do this to remove duplicate tasks, like installing the same dependency twice in different places. This rule isn't necessary but does set us up for rule #3.
298
299</Collapsible>
300
301<Collapsible summary="3. Different versions of dependencies must be installed in the app node_modules directory">
302
303In the [Modify the Metro config](#modify-the-metro-config) step, we instructed Metro to do a couple of this, specifically:
304
305- #2 - Resolve dependencies in the order of the local **/apps/&lt;name&gt;/node_modules** and root **/node_modules** directories.
306- #3 - Disable resolving dependencies using the hierarchical lookup strategy.
307
308If a workspace uses a different library version than the one installed in the root **/node_modules**, that different library version must be installed in the workspace **/apps/&lt;name&gt;/node_modules** directory.
309
310When Metro resolves a library, for example, `react`, from the workspace, it should find that different version in **/apps/&lt;name&gt;/node_modules** and not look inside the root **/node_modules** directory.
311
312When importing a dependency from the root **/node_modules** folder that also imports `react`, `react` should still resolve to the different version installed in **/apps/&lt;name&gt;/node_modules**. That's what the disabled hierarchical lookup option does for Metro. Without this, some libraries might import the wrong `react` version and cause "multiple React versions found" errors.
313
314</Collapsible>
315
316The default settings of tools like [pnpm](https://pnpm.io/) do not follow these rules. You can change that by adding a **.npmrc** file with `node-linker=hoisted` ([see docs](https://pnpm.io/npmrc#node-linker)). That config option will change the behavior to match these rules.
317
318### Script '...' does not exist
319
320React Native uses packages to ship both JavaScript and native files. These native files also need to be linked, like the [**react-native/react.Gradle**](https://github.com/facebook/react-native/blob/v0.70.6/react.gradle) file from **android/app/build.Gradle**. Usually, this path is hardcoded to something like:
321
322**Android** ([source](https://github.com/facebook/react-native/blob/e918362be3cb03ae9dee3b8d50a240c599f6723f/template/android/app/build.gradle#L84))
323
324```groovy
325apply from: "../../node_modules/react-native/react.gradle"
326```
327
328**iOS** ([source](https://github.com/facebook/react-native/blob/e918362be3cb03ae9dee3b8d50a240c599f6723f/template/ios/Podfile#L1))
329
330```ruby
331require_relative '../node_modules/react-native/scripts/react_native_pods'
332```
333
334Unfortunately, this path can be different in monorepos because of [hoisting](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/). It also doesn't use the [Node module resolution](https://nodejs.org/api/modules.html#all-together). You can avoid this issue by using Node to find the location of the package instead of hardcoding this:
335
336**Android** ([source](https://github.com/expo/expo/blob/6877c1f5cdca62b395b0d5f49d87f2f3dbb50bec/templates/expo-template-bare-minimum/android/app/build.gradle#L87))
337
338```groovy
339apply from: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../react.gradle")
340```
341
342**iOS** ([source](https://github.com/expo/expo/blob/61cbd9a5092af319b44c319f7d51e4093210e81b/templates/expo-template-bare-minimum/ios/Podfile#L2))
343
344```ruby
345require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
346```
347
348In the snippets above, you can see that we use Node's own [`require.resolve()`](https://nodejs.org/api/modules.html#requireresolverequest-options) method to find the package location. We explicitly refer to `package.json` because we want to find the root location of the package, not the location of the entry point. And with that root location, we can resolve to the expected relative path within the package. [Learn more about these references here](https://github.com/expo/expo/blob/4633ab2364e30ea87ca2da968f3adaf5cdde9d8b/packages/expo-modules-core/README.mdx#importing-native-dependencies---autolinking).
349
350All Expo SDK modules and templates, starting from SDK 43, have these dynamic references and work with monorepos. But, occasionally, you might run into packages that still use the hardcoded path. You can manually edit it with [patch-package](https://github.com/ds300/patch-package#readme) or mention this to the package maintainers.
351
352### Remove expo-yarn-workspaces
353
354Before SDK 43, `expo-yarn-workspaces` was the recommended way to use Yarn workspaces with your Expo project. It was used to symlink all required dependencies back to the app's **node_modules** folder. Although this works for most apps, it has some flaws. For example, it doesn't work well with multiple versions of the same package.
355
356We made some significant changes with Expo SDK 43 to improve support for monorepos. [The auto linker in the newer Expo modules](https://blog.expo.dev/whats-new-in-expo-modules-infrastructure-7a7cdda81ebc) now also looks for packages in parent **node_modules** folders. None of our native files inside our template contain hardcoded paths to packages.
357
358If you are following this guide, you should remove that package from your project's dependencies.
359