Using yarn link with React Native/metro bundler

2 minute read

A quick note from the trenches of React Native & Expo. While working on Meet Kinksters’ native app, I needed to develop a small JavaScript library to shim in SDK support for Synapse refresh tokens.

To do so, I decided to leverage yarn link, a quick CLI shortcut for creating and managing yarn dependency symlinks. After doing so, I started to get errors in Expo/React Native’s metro bundler:

expo_1  | Unable to resolve module synapse-fetch-mw from /build/App.js: synapse-fetch-mw could not be found within the project or in these directories:
expo_1  |   node_modules
expo_1  | 
expo_1  | If you are sure the module exists, try these steps:
expo_1  |  1. Clear watchman watches: watchman watch-del-all
expo_1  |  2. Delete node_modules and run yarn install
expo_1  |  3. Reset Metro's cache: yarn start --reset-cache
expo_1  |  4. Remove the cache: rm -rf /tmp/metro-*
expo_1  |   29 | import matrixSdk, {Method} from 'matrix-js-sdk';
expo_1  |   30 | import RequestWrapper from "./lib/request";
expo_1  | > 31 | import SynapseRefresh from "synapse-fetch-mw";

Weird. I could see the symlink was successfully created on disk, so I figured the bundler must not “see” it. Turns out this is a long-standing issue in Metro, with no real definitive fix in sight. Some commenters suggested various workarounds of varying hackishness, and many people claimed (surprise, surprise) that clearing caches per the instructions would solve the problem. No dice.

Given I am not an expert JavaScript developer and try to avoid bundler dark arts, I was skeptical of going too far down this rabbit hole. But the issues did confirm the bundler was to blame. I found this package, which encapsulates logic to make symlinks “appear” to Metro, and applied it.

The project README includes some example configuration, but I found I had to tweak it some to work with the default ejected Expo metro.config.js:

--- a/app/metro.config.js
+++ b/app/metro.config.js

-module.exports = getDefaultConfig(__dirname);
+const { applyConfigForLinkedDependencies } = require('@carimus/metro-symlinked-deps');
+
+module.exports = applyConfigForLinkedDependencies(
+  getDefaultConfig(__dirname),
+  {
+    projectRoot: __dirname,
+    blacklistLinkedModules: ['react-native'],
+    resolveNodeModulesAtRoot: true,
+  },
+);

Without specifying resolveModulesAtRoot, I found that requirements (TypeScript-related?) in the linked package wouldn’t resolve.

Unable to resolve module @babel/runtime/helpers/interopRequireDefault from /mnt/synapse-fetch-mw/src/fetch-wrapper.ts: @babel/runtime/helpers/interopRequireDefault could not be found within the project.

Again, web searches came up with a lot of low-quality results. I made sure @babel/runtime was explicitly required at the same version as @babel/core, and changed resolveNodeModulesAtRoot from its default value of false.

Hope this helps you hack away on dependencies a little easier!

Updated: