facebook / metro

🚇 The JavaScript bundler for React Native
https://metrobundler.dev
MIT License
5.24k stars 626 forks source link

Experimental symlink support not working for PNPM Monorepo #1042

Open brownieboy opened 1 year ago

brownieboy commented 1 year ago

Description

I have PNPM working with the new, experimental symlink support in RN 0.72.3. But it only works for me for a single RN package in a repo. It doesn't work for me in a Monorepo set up.

In my monorepo pnpm ios from the apps/app1 folder throws this error:

 BUNDLE  ./index.js

error: Error: Unable to resolve module react-native from /Users/michaelbrown/Development/test-rn72-pnpm-monorepo/apps/app1/index.js: react-native could not be found within the project or in these directories:
  node_modules
  ../../node_modules
  3 |  */
  4 |
> 5 | import {AppRegistry} from 'react-native';
    |                            ^
  6 | import App from './App';
  7 | import {name as appName} from './app.json';

pnpm android throws this error:

FAILURE: Build failed with an exception.

* Where:
Settings file '/Users/michaelbrown/Development/test-rn72-pnpm-monorepo/apps/app1/android/settings.gradle' line: 2

* What went wrong:
A problem occurred evaluating settings 'app1'.
> Could not read script '/Users/michaelbrown/Development/test-rn72-pnpm-monorepo/apps/app1/node_modules/@react-native-community/cli-platform-android/native_modules.gradle' as it does not exist.

react-native is in the apps/app1/node_modules folder, although it's a symlink (which is how PNPM works). That android/settings.gradle file also is there, and not as a symlink this time.

React Native Version

0.72.3

Output of npx react-native info

npx react-native info info Fetching system and libraries information... System: OS: macOS 13.3.1 CPU: (8) arm64 Apple M1 Memory: 68.48 MB / 16.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: version: 18.9.0 path: ~/Library/Caches/fnm_multishells/87821_1689716195576/bin/node Yarn: version: 1.22.19 path: ~/Library/Caches/fnm_multishells/87821_1689716195576/bin/yarn npm: version: 8.19.1 path: ~/Library/Caches/fnm_multishells/87821_1689716195576/bin/npm Watchman: version: 2023.06.12.00 path: /opt/homebrew/bin/watchman Managers: CocoaPods: version: 1.12.1 path: /Users/michaelbrown/.rvm/gems/ruby-3.0.0/bin/pod SDKs: iOS SDK: Platforms:

Steps to reproduce

mkdir test-rn72-pnpm-monorepo
cd test-rn72-pnpm-monorepo
mkdir apps
cd apps
npx react-native@latest init app1
cd app1
rm yarn.lock
rm -rf node_modules
cd ../..
npm init -y

Add new file, pnpm-workspace.yaml at root repo level:

packages:
  # all packages in direct subdirs of packages/
  - 'apps/*'

That file is required for PNPM monorepo support, as PNPM does not use the packages field in the package.json.

Run pnpm i to (re)install the dependencies.

Enable the experimental symlink support in file apps/app1/metro.config.js:

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

/**
 * Metro configuration
 * https://facebook.github.io/metro/docs/configuration
 *
 * @type {import('metro-config').MetroConfig}
 */
const config = {
  resolver: {
    unstable_enableSymlinks: true,
    unstable_enablePackageExports: true,
  },
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

Snack, code example, screenshot, or link to a repository

https://github.com/brownieboy/test-rn72-pnpm-monorepo

cortinico commented 1 year ago

Moving to metro

uxigene commented 1 year ago

Managed to get it working by adding watchFolders. Here is my config example:

const config = {
  watchFolders: [path.join(__dirname, '..', '..')],
  resolver: {
    unstable_enableSymlinks: true,
    unstable_enablePackageExports: true,
  },
};
brownieboy commented 1 year ago

@uxigene,

  watchFolders: [path.join(__dirname, '..', '..')],

Many thanks! Yes, that worked for me too 😊, although I'm not sure why that works 🤔.

You need to add the path import at the top of the file (metro.config.js) too, of course:

const path = require('path');
MartinDawson commented 1 year ago

This doesn't seem to work for me, same error as the above on android, even after clearing builds, cache etc, @brownieboy @uxigene please can you update the repo with the working builds.

Also, can you please say specifically what node version, pnpm version you are using.

It seems very flaky.

karlhorky commented 10 months ago

For those using Expo SDK 50 + RN 0.73 with pnpm, it seems there are other file resolution issues too:

lbxa commented 1 month ago

Okay I've been wrestling with getting the full pnpm experience with React Native without making any compromises like node-linker=hoisted. The DX you get from pnpm in the long-term justifies losing 2 days on this.

I could only get my expo router app back up and running after going all in on Metro. Big shout out to this guide.

I'm guessing most folks who want to place their React Native projects inside a pnpm monorepo are also sharing code from tailwind packages etc. So here's my finished article. With this config pnpm monorepo's work like a charm. I've included my Nativewind and SVG transformer configs to show how easily you can extend Metro without accidentally breaking something else.

{
  ...
  "expo": "^51.0.17",
  "expo-router": "~3.5.23",
  "react-native": "^0.74.5",
  ...
}
// Learn more: https://docs.expo.dev/guides/monorepos/
const path = require("path");
const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
const { withNativeWind } = require("nativewind/metro");
const { makeMetroConfig } = require("@rnx-kit/metro-config");
const { mergeConfig } = require("metro-config");
const MetroSymlinksResolver = require("@rnx-kit/metro-resolver-symlinks");

const symlinksResolver = MetroSymlinksResolver();

const projectDir = __dirname;
const monorepoRoot = path.resolve(projectDir, "../..");
const defaultConfig = getDefaultConfig(projectDir);

/** @type {import('expo/metro-config').MetroConfig} */
const monorepoConfig = {
  resolver: {
    disableHierarchicalLookup: true,
    nodeModulesPaths: [
      path.resolve(projectDir, "node_modules"),
      path.resolve(monorepoRoot, "node_modules"),
    ],
    /**
     * React Native has very frail symlink support for modern monorepo tools  
     * that rely on symlinks and global caches to dramatically increase the
     * performance of installs e.g. pnpm. The best way around this is using
     * Microsoft's rnx-kit. I've written more extensively about this in the
     * README.
     * 
     * @see https://gist.github.com/Zn4rK/ed60c380e7b672e3089074f51792a2b8
     */
    resolveRequest: (context, moduleName, platform) => {
      try {
        // Symlinks resolver throws when it can't find what we're looking for.
        const res = symlinksResolver(context, moduleName, platform);

        if (res) {
          return res;
        }
      } catch {
        /**
         * If we have an error, we pass it on to the next resolver in the chain,
         * which should be one of expos.
         * @see https://github.com/expo/expo/blob/9c025ce7c10b23546ca889f3905f4a46d65608a4/packages/%40expo/cli/src/start/server/metro/withMetroResolvers.ts#L47
         */
        return context.resolveRequest(context, moduleName, platform);
      }
    },
  },
  /**
   * Add the monorepo paths to the Metro config.
   * This allows Metro to resolve modules from the monorepo.
   *
   * @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config
   */
  watchFolders: [monorepoRoot],
  /**
   * Move the Metro cache to the `node_modules/.cache/metro` folder.
   * This repository configured Turborepo to use this cache location as well.
   * If you have any environment variables, you can configure Turborepo to invalidate it when needed.
   * @see https://turbo.build/repo/docs/reference/configuration#env
   */
  cacheStores: [
    new FileStore({
      root: path.join(projectDir, "node_modules", ".cache", "metro"),
    })
  ]
};

/** @type {import('expo/metro-config').MetroConfig} */
const svgConfig = {
  resolver: {
    assetExts: defaultConfig.resolver.assetExts.filter((ext) => ext !== "svg"),
    sourceExts: [...defaultConfig.resolver.sourceExts, "svg"],
  },
  transformer: {
    // <3 -> https://github.com/kristerkari/react-native-svg-transformer/issues/141
    assetPlugins: ['expo-asset/tools/hashAssetFiles'],
    babelTransformerPath: require.resolve('react-native-svg-transformer/expo'),
  },
};

/**
 * Merging configs do not deeply merge arrays/functions. Keep this in mind to not
 * override important properties. Order matters!
 * 
 * @see https://metrobundler.dev/docs/configuration/#merging-configurations
 */
const finalConfig = makeMetroConfig(mergeConfig(defaultConfig, monorepoConfig, svgConfig));

/**
 * Nativewind config must come last! Internally it uses withCssInterop to 
 * resolve css imports. If this is overridden, Nativewind will not work.
 * 
 * @see https://github.com/nativewind/nativewind/issues/972#issuecomment-2329660147
 *
 * Striking a balance between Nativewind and rnx-kit was tricky
 * 
 * @see https://github.com/nativewind/nativewind/issues/926
 */
module.exports = withNativeWind(finalConfig, {
  input: path.join(projectDir, "./src/global.css"),
  configPath: path.join(projectDir, "./tailwind.config.ts")
});
karlhorky commented 1 month ago

For official first-class support in Expo, it looks like non-hoisted pnpm support is scheduled for Expo SDK 52 - mentioned in @byCedric's post in the pnpm repo:

brownieboy commented 2 weeks ago

I'm revisiting this issue with React Native 0.76.1, and I still can't build with PNPM.

The experimental symlink support in Metro is now experimental no longer, and is enabled by default, which is good. But the earlier workaround with watchFolders: [path.join(__dirname, '..', '..')], doesn't work any more.

I'm not using Expo.

Current error in Xcode is:

RunLoopObserver 'react/debug/react_native_assert.h' file not found

When I temporarily swapped out PNPM for Yarn, my app would build and launched the Simulator, so I'm pretty sure that it's (still) and PNPM/Symlink issue.

Update Actually, with an empty React Native text app, Xcode does build with PNPM in a monorepo. (I should have checked that first, of course.)

The errors above are something more specific to my actual app. I ended up going back to Yarn to work around it.

robhogan commented 2 weeks ago

@brownieboy Metro’s symlink support is stable and enabled, but Metro only bundles your JavaScript and assets.

If you’re seeing errors about missing headers from Xcode, that’s not Metro. The RN community CLI may not fully support symlinks with native dependency autolinking.