How to develop React Native in workspaces
The article would help you setup to develop react native project in mono repo.
Workspaces
You can see this link to get the concept.
We use yarn workspaces in this guide.
What’s the problem
Well, when resolving modules, react-native-cli
doesn't search modules in parent node_modules
folder outside projectRoot
(that directory you run the command by default), unlike native builtin require(..)
of Node.js or webpack.
For example,
├── node_modules
│ ├── A -> packages/A
│ └── B -> packages/B
└── packages
├── A // A import B
└── B
If we try to build bundle in packages/A
folder, it would fail because it cannot find the module B
So, we need to some work to do.
Environments
yarn@1.22.5
Node.js@14.16.0
react-native@0.59.9
Possible approaches
Before starting, there are several things we should keep in mind.
- The
react-native
packager would crawl those files in rootProject and watchFolders. - It only allows one copy of
react-native
to be included, otherwise the duplicate module issue would come out:
Loading dependency graph...jest-haste-map: @providesModule naming collision: Duplicate module name: react-nativeLoading dependency graph...jest-haste-map: @providesModule naming collision: Duplicate module name: react-native
- It ignores symlinks, no matter we enable watchman or not
- It don’t search module in parent
node_modules
Approach 1. Don’t hoist any dependency
First thought is to prevent hoisting any dependencies needed by A
regardless of their level, including package B
. So we would get all files in the packages/A
folder.
After yarn install
, we got:
├── lerna.json
├── package.json
├── packages
│ ├── A
│ │ ├── node_modules
│ │ │ └── B -> ../../B
│ │ ├── index.js
│ │ └── package.json
│ └── B
│ ├── index.js
│ └── package.json
└── yarn.lock
IMPORTANT NOTICE
If you somehow ran into the issue Error: ENOENT: no such file or directory, lstat … after running
yarn install
and couldn't resolve it unfortunately, then you should say goodbye to this approach
Why add ./packages
into watchFolders
? Because the packager ignore symlink, do that to avoid the cannot-resolve-module issue:
error: bundling failed: Error: Unable to resolve module `B` from `.../packages/A/index.js`: Module `B` does not exist in the Haste module map
Sure. You can work around this with patch. And the bundler cannot watch the files’ changing and rebuild automatically
Last thing you should notice, if B/node_modules/react-native
exist , then you should ignore the one under B. The config would be more complicated:
blacklistRE
: ignore otherreact-native
to avoid the duplicate module issueextraNodeModules
: tells resolver to resolvereact-native
for files in B folder toA/node_modules/react-native
, since we already ignored that one inB/node_modules
Done. You can start your work.
Approach 2. Hoist anything except react-native
watchFolders
: notice that we add ROOT folder intowatchFolders
instead.
So the keynote is
- Make other workspaces crawlable through adding them into
watchFolder
- Make sure only one copy of
react-native
module is crawled
Another little issue of iOS production bundling
What happens when we build the react-native bundle?
- Setup the packager
- Build the iOS/android JavaScript bundle
- Copy the assets (images) used by the bundle above to specific folder
The processed of step 3 on iOS and android are different.
- For iOS, it would keep the relative paths of assets to project root (cwd).
- For android, all imported assets would be placed in one same folder.
For example, we imported an image in B
and an image in A
for comparison:
├── packages
│ ├── deep
│ │ └── A
│ │ ├── node_modules
│ │ │ └── B -> ../../B
│ │ ├── index.js
│ │ └── package.json
│ └── B
│ ├── index.js
│ └── package.json
└── yarn.lock
after running the following command in packages/deep/A
react-native bundle --platform ios --entry-file ./index.js --bundle-output dist/index.ios.js --assets-dest dist
the result would be strange:
├── B
│ └── assets
│ └── bar.png
└── dist
├── assets
│ └── assets
│ └── foo.png
└── index.ios.js
The assets used by module B
are placed outside the dist
folder. So we should change the behavior, by adding our custom asset plugin
Then, the output would be:
dist
├── assets
│ ├── _
│ │ └── _
│ │ └── B
│ │ └── assets
│ │ └── bar.png
│ └── assets
│ └── foo.png
└── index.ios.js
Everything is OK now.