Coder Social home page Coder Social logo

Comments (39)

fiznool avatar fiznool commented on May 3, 2024 59

I'm seeing the same thing too @Edmond-XavierCollot - thank you @denwakeup for the fix. Here's an updated metro.config.js file that should be used for RN 0.59+.

const path = require('path');

module.exports = {
  /**
   * Ensure any imports inside the shared 'components' folder resolve to the local node_modules folder
   */
  resolver: {
    extraNodeModules: new Proxy({}, {
      get: (target, name) => path.join(process.cwd(), `node_modules/${name}`),
    }),
  },
 /**
   * Add our workspace roots so that react native can find the source code for the included packages
   * in the monorepo
   */
  projectRoot: path.resolve(__dirname),
  watchFolders: [
    path.resolve(__dirname, "../../components"),
    path.resolve(__dirname, "../../node_modules")
  ]
}

from metro.

fiznool avatar fiznool commented on May 3, 2024 45

We actually ended up adding nohoist: ['**'] to our react-native yarn workspace because it was easier than figuring out which needed to be where.

I came to the same conclusion too! Yes I end up with a larger workspace but the benefits of not having to hunt these things down far outweigh the cons of a larger workspace size.

As mentioned above, for RN 0.57 you have to put your extraNodeModules inside a resolver object in your rn-cli.config.js, and also getProjectRoots() is no longer a thing. The example provided above would be rewritten as follows for RN 0.57+:

module.exports = {
  /**
   * Add "global" dependencies for our RN project here so that our local components can resolve their
   * dependencies correctly
   */
  resolver: {
    extraNodeModules: {
      react: path.resolve(__dirname, "node_modules/react"),
      "react-native": path.resolve(__dirname, "node_modules/react-native"),
      "@storybook": path.resolve(__dirname, "node_modules/@storybook")
    }
  },
 /**
   * Add our workspace roots so that react native can find the source code for the included packages
   * in the monorepo
   */
  projectRoot: path.resolve(__dirname),
  watchFolders: [
    path.resolve(__dirname, "../../components"),
    path.resolve(__dirname, "../../node_modules")
  ]
}

from metro.

jonbronson avatar jonbronson commented on May 3, 2024 39

Has there been any progress on this issue? It seems to be a major show stopper for a unified codebase.

from metro.

jeanlauliac avatar jeanlauliac commented on May 3, 2024 17

If I add a package.json to common and install react there it works. So I guess this has to do with the packager walking the file upwards and not finding node_modules as it's in a sibling directory and not a parent directory?

Yes. If you do require('foo'), then it looks node_modules/foo first in the directory of the module you're requiring from, then for each parent directory in turn. So if there's a node_module somewhere in another nested folder it won't find it. The roots are not used for the resolution of node-style packages, purposefully. This resolution pattern follow closely the algorithm specified by Node.js: https://nodejs.org/api/modules.html#modules_all_together.

So one solution is to have all common dependencies be installed in the root directory, that can be resolved from anywhere. Another solution might be to use the extraNodeModules field to specify things you want available manually. Ex. something like:

  extraNodeModules: {
    'foobar': path.resolve(__dirname, '../common/node_modules/foobar'),
  },

from metro.

willgriffiths avatar willgriffiths commented on May 3, 2024 12

@reggie3 @CompuIves
Here is the link to the extraNodeModules documentation: https://facebook.github.io/metro/docs/en/configuration#resolver-options

@jeanlauliac's syntax is the one I've been using.

I'm working in a monorepo with yarn workspaces. I'm using a combination of getProjectRoots to tell metro where to find packages. And then using extraNodeModules to make sure that the local package goes looking for the react, react-native, etc module in the right place.

module.exports = {
  /**
   * Add "global" dependencies for our RN project here so that our local components can resolve their
   * dependencies correctly
   */
  extraNodeModules: {
    react: path.resolve(__dirname, "node_modules/react"),
    "react-native": path.resolve(__dirname, "node_modules/react-native"),
    "@storybook": path.resolve(__dirname, "node_modules/@storybook")
  },
  getProjectRoots() {
    /**
     * Add our workspace roots so that react native can find the source code for the included packages
     * in the monorepo
     */
    const projectPath = path.resolve(__dirname);
    const componentsPath = path.resolve(__dirname, "../../components");
    const rootModulesPath = path.resolve(__dirname, "../../node_modules");

    return [
      projectPath,
      componentsPath,
      rootModulesPath
    ];
 }
}

My use case is probably not the same as yours but I hope it helps.

Also I'm using "react-native": "0.55.4".

from metro.

fredbt avatar fredbt commented on May 3, 2024 11

So I managed to make this work following these steps:

  1. Added a package.json to my "common" folder.

  2. In the dependencies of my "app1" folder, I added:

"dependencies": {
"common": "file:../common/"
}

My folder organization is:
-- app1: app to do X for users of type 1
-- app2: app to do X for users of type 2
-- common

I'm sure there's something bad about this structure because I ended up with different node_modules folders:
--- one under app1:
---> this one includes "common".
--- another under common
--- another under app2

Not sure about the side effects here (larger binary?) but this works for now.

from metro.

brentvatne avatar brentvatne commented on May 3, 2024 11

@rafeca - can we get some docs on the above? it's painful to update to the latest version of react-native on a project that uses rn-cli.config.js right now

from metro.

dagatsoin avatar dagatsoin commented on May 3, 2024 11

For the record, my final config file for making Storybook find the upper folder

var path = require("path");
var config = {
  projectRoot: path.resolve(__dirname),
  watchFolders: [
    // Let's add the root folder to the watcher
    // for live reload purpose
    path.resolve(__dirname, "../src"),
  ],
  resolver: {
    extraNodeModules: {
      // Here I reference my upper folder
      "@sproutch/ui": path.resolve(__dirname, "../src"),
      // Important, those are all the dependencies
      // asked by the "../src" but which
      // are not present in the ROOT/node_modules
      // So install it in your RN project and reference them here
      "expo": path.resolve(__dirname, "node_modules/expo"),
      "lodash.merge": path.resolve(__dirname, "node_modules/lodash.merge"),
      "react": path.resolve(__dirname, "node_modules/react"),
      "reactxp": path.resolve(__dirname, "node_modules/reactxp"),
      "react-native": path.resolve(__dirname, "node_modules/react-native")
    }
  }
}
module.exports = config;

from metro.

nihar108 avatar nihar108 commented on May 3, 2024 7

The solution provided by @fiznool works. But in RN59, rn-cli.config.js is replaced by metro.config.js

from metro.

willgriffiths avatar willgriffiths commented on May 3, 2024 6

@jkillian

This setup works when used with yarn nohoist. nohoist allows you to force modules to stay in project folders and stop them from being hoisted up to the monorepo root.

RN doesn't work if react-native gets hoisted. Therefore, we needed to nohoist it to get RN working in our monorepo.

Nohoist is how you can guarantee that the modules end up insider your RN project folder.

Also, any library that has native code has to be no hoisted so that you can react-native link it.

Be careful though, no hoisting everything reduces the benefits of yarn workspaces and increases install times so I only use it if I have to.

My team and I are hoping that metro (and as a side note: react-scripts) start working nicer with yarn workspaces.

The solution @willgriffiths and @Strate have posted is confusing to me: why do you have to tell metro to look in the current directory's node_modules directory? Isn't that what it does by default?

In RN if you import a component from "../../components/Button" and that Button component imports View "react-native", metro will go looking for "react-native" in the Button package as per the normal module resolution rules.

It starts in the component's folder "../../components/Button/node_modules/react-native" and if it doesn't find it node_modules it climbs up a level checks in node_modules .

  1. "../../components/Button/node_modules/react-native"
  2. "../../components/node_modules/react-native"
  3. "../node_modules/react-native"
  4. etc

By telling metro that react and react-native are located inside the React Native project folder then you override this behaviour so it goes looking in:

  1. "RNProject/node_modules/react-native"

And do you have to do this for every single dependency? If so, issues can arise in a workspace setup, because there's no guarantee if a dependency will be installed at the top-level node_modules or in a workspace's node_modules directory

I start no hoisting or adding entries to extra node modules when I get a metro errors like:

  • duplicate modules in haste module map
  • can't find module in haste module map

from metro.

flybayer avatar flybayer commented on May 3, 2024 6

Here's my solution for workspaces and importing shared code (on a non-Expo app)

// package.json
  "devDependencies": {
    ...
    "expo-yarn-workspaces": "1.2.1"
  },
  "workspaces": {
    "nohoist": [
      "react",
      "react-native",
      "react-native-*",
      "react-native/**",
      "expo-yarn-workspaces"
    ]
  }
// metro.config.js
const { createMetroConfiguration } = require("expo-yarn-workspaces")

const config = createMetroConfiguration(__dirname)

// Make react-native import from files in other workspace resolve to node_modules in this dir
config.resolver.extraNodeModules["react"] = `${__dirname}/node_modules/react`
config.resolver.extraNodeModules["react-native"] = `${__dirname}/node_modules/react-native`

// Default metro config
config.transformer.getTransformOptions = async () => ({
  transform: {
    experimentalImportSupport: false,
    inlineRequires: false,
  },
})

module.exports = config

from metro.

Strate avatar Strate commented on May 3, 2024 5

@willgriffiths thank you for that solution. Based on yours, I made a more generic one:

const path = require("path");

const installedDependencies = require("./package.json").dependencies;

const extraNodeModules = {};
Object.keys(installedDependencies).forEach(dep => {
  extraNodeModules[dep] = path.resolve(__dirname, "node_modules", dep);
});

module.exports = {
  extraNodeModules: extraNodeModules
};

from metro.

RomualdPercereau avatar RomualdPercereau commented on May 3, 2024 4

With the 0.57 of react-native, it looks like the extraNodeModules feature doesn't work anymore?

from metro.

rafeca avatar rafeca commented on May 3, 2024 4

In RN 0.57 the format of the Metro config options has changed slightly, now the extraNodeModules param is inside the resolver field (there's more information in the metro docs).

@RomualdPercereau are you configuring the extraNodeModules?

from metro.

GanchoDanailov avatar GanchoDanailov commented on May 3, 2024 4

"dependencies": {
"common": "file:../common/"
}
+ npm link
works as a workaround

from metro.

pmierzejewski avatar pmierzejewski commented on May 3, 2024 3

@willgriffiths and @Strate solutions work great. If you're using TS don't forget to add your shared code path to

 "rootDirs": [
            ".",
            "../shared"
        ] /* List of root folders whose combined content represents the structure of the project at runtime. */,

in tsconfig.json as well.

from metro.

ivan-jorge001 avatar ivan-jorge001 commented on May 3, 2024 3

i am in version RN0.57, latest version i still have this issue, any solutions? none of the solutions aboved worked

from metro.

Edmond-XavierCollot avatar Edmond-XavierCollot commented on May 3, 2024 3

The solution of @fiznool works but if I import a file that needs transpiling by babel I got an error Unable to resolve "@babel/runtime/helpers/interopRequireDefault" from "../../outsideFile.js"

And adding "@babel": path.resolve(__dirname, "node_modules/@babel") in extraNodeModules does not work.

Any idea ?

from metro.

slorber avatar slorber commented on May 3, 2024 3

Something that worked for me, mixture of existing solutions, for an Expo app that is not yet part of a monorepo:

  "resolutions": {
    "@company/api": "link:../../frontend/packages/api",
    "@company/config": "link:../../frontend/packages/config",
  },
  "dependencies": {
    "@company/api": "link:../../frontend/packages/api",
    "@company/config": "link:../../frontend/packages/config",
  }
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
const { mapValues } = require('lodash');

// Add there all the Company packages useful to this app
const CompanyPackagesRelative = {
  '@company/config': '../../frontend/packages/config',
  '@company/api': '../../frontend/packages/api',
};

const CompanyPackages = mapValues(CompanyPackagesRelative, (relativePath) =>
  path.resolve(relativePath),
);


// Inspired by expo-yarn-workspace
// https://github.com/expo/expo/blob/master/packages/expo-yarn-workspaces/index.js
function createMetroConfiguration(projectPath) {
  projectPath = path.resolve(projectPath);

  const defaultConfig = getDefaultConfig(projectPath);

  const watchFolders = [
    ...Object.values(CompanyPackages),
    ...defaultConfig.watchFolders,
  ];
  const extraNodeModules = {
    ...CompanyPackages,
    ...defaultConfig.resolver.extraNodeModules,
  };

  // Should fix error "Unable to resolve module @babel/runtime/helpers/interopRequireDefault"
  // See https://github.com/facebook/metro/issues/7#issuecomment-508129053
  // See https://dushyant37.medium.com/how-to-import-files-from-outside-of-root-directory-with-react-native-metro-bundler-18207a348427
  const extraNodeModulesProxy = new Proxy(extraNodeModules, {
    get: (target, name) => {
      if (target[name]) {
        return target[name];
      } else {
        return path.join(projectPath, `node_modules/${name}`);
      }
    },
  });

  return {
    ...defaultConfig,
    projectRoot: projectPath,
    watchFolders,
    resolver: {
      ...defaultConfig.resolver,
      extraNodeModules: extraNodeModulesProxy,
    },
  };
}

module.exports = createMetroConfiguration(__dirname);;

from metro.

zth avatar zth commented on May 3, 2024 2

Tested with fresh react-native init and version 0.45, same issue still. How does the packager resolve modules? Is it somehow relative to the file being required, not primarily from the project roots...?

from metro.

charlie-axsy avatar charlie-axsy commented on May 3, 2024 2

@fredbt yes they do. But that's not very useful when you're working on one of your common components. Every time you save the file you need to run npm install. It's not a very nice workflow at all

from metro.

rafeca avatar rafeca commented on May 3, 2024 2

Hi!

We added a util method in the metro-config package that should help migrating the old configuration format to the new one.

You can call it by doing:

const {convert} = require('metro-config');

module.exports = convert.convertOldToNew({ /* old config object */});

This works as a temporary workaround when upgrading to the latest metro version.

from metro.

denwakeup avatar denwakeup commented on May 3, 2024 2

@Edmond-XavierCollot
I had same problem. @jgcmarins solution works great for me.

from metro.

theneekz avatar theneekz commented on May 3, 2024 2

For non expo users using this in the root folder package.json:

"workspaces": {
  "packages" : [...],
  "noHoist": ["**"] //keep node_modules in the children folders

here was the metro.config.js that worked for me:

const path = require("path");

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },

  /**
  * Ensure any imports inside the shared folder resolve to the local
  * node_modules folder
  */
  resolver: {
    extraNodeModules: new Proxy({}, {
      get: (target, name) => path.join(process.cwd(), `node_modules/${name}`),
    }),
  },

   /**
   * Add our workspace roots so that react native can find the source code for
   * the included packages in the monorepo
   */
  projectRoot: path.resolve(__dirname),
  watchFolders: [
    path.resolve(__dirname, "../common"),
    path.resolve(__dirname, "../../node_modules")
  ]
};

from metro.

fredbt avatar fredbt commented on May 3, 2024 1

Hi @zth I'm facing a similar issue here.

So, if you do the following setup:

  1. Make "common" a JS package:
  • this would require a package.json for the common folder, and potentially an index.js exporting your common classes.
  1. Add a rn-cli.config.js to your app folder.

Does it work? I agree it sucks to have two node_modules folder, but this could be a temporary solution for me until there's a proper solution from metro.

from metro.

jkillian avatar jkillian commented on May 3, 2024 1

Thanks for the detailed info @willgriffiths 😄

Your example with importing "../../components/Button" is interesting! I believe you're doing cross-workspace relative imports here. I may need to accomplish the exact same thing in my situation, so your solution seems perfect.

Be careful though, no hoisting everything reduces the benefits of yarn workspaces and increases install times so I only use it if I have to.

Yep, agreed with the downsides. We actually ended up adding nohoist: ['**'] to our react-native yarn workspace because it was easier than figuring out which needed to be where. (It's possible that the install times might grow to be obnoxious, we'll see.)

My team and I are hoping that metro (and as a side note: react-scripts) start working nicer with yarn workspaces.

Yes, I hope so too! So far it's been a bit of a hassle to get the react-native toolchain (metro) to work in a nice way with our old web-only yarn workspaces setup. Hopefully FB will have the resources to improve things, maybe the metro team and the yarn team can have a nice fun offsite together and work on these issues 😆 (Thanks to both teams for all the work they've put in to their tools so far!)

from metro.

mmazzarolo avatar mmazzarolo commented on May 3, 2024 1

I didn't know about the convertOldToNew utils, looks good!

About the docs: I meant adding it in the release notes the next time (since it's release-specific).

That said, I'll try to write a few lines in the weekend 👍

from metro.

jc-murray-1986 avatar jc-murray-1986 commented on May 3, 2024

@fredbt there's a major problem with your solution. If you update a file in common, then the changes are not reflected (and loaded) into app1. app1 still has the old version of the file.

Like you say, there is a copy of common in app1/node_modules/common. The copy is not updated when you edit the original lib.

from metro.

fredbt avatar fredbt commented on May 3, 2024

Thanks @jc-murray-1986
But the packages get updated if I run npm install, correct?

from metro.

jl-compileit avatar jl-compileit commented on May 3, 2024

We're having the same issue with a monorepo structure where we currently have web/mobile/shared folders. In our case the root also has a package.json to keep a single version of things like ESLint and Relay and importing those dependencies in the app doesn't work either. The web app is built with webpack and can handle this fine just by configuring module roots and/or module aliases.

Adding every possible module directory to an rn-cli-config.js doesn't seem to make a difference:

const path = require('path');

module.exports = {
  getProjectRoots() {
    return [
      // Keep the project directory.
      path.resolve(__dirname, './'),
      path.resolve(__dirname, './node_modules'),

      // Add the common root.
      path.resolve(__dirname, '../'),
      path.resolve(__dirname, '../node_modules'),

      // Add shared.
      path.resolve(__dirname, '../shared'),
      path.resolve(__dirname, '../shared/node_modules'),
    ];
  },
};

Our initial solution was using shared as a local dependency ("file" in package.json which creates symlinks) and each package being completely separate. This kind of worked but caused some weird install issues where dependencies from shared got added or removed depending on where the install ran and having to manage multiple versions and configs of things like ESLint was really annoying.

from metro.

reggie3 avatar reggie3 commented on May 3, 2024

@jeanlauliac can you provide more information about your solution? The extraNodeModules link now resolves to a 404 page.

from metro.

jeanlauliac avatar jeanlauliac commented on May 3, 2024

I moved on onto other projects so I'm not sure what the status of this field in the configuration at the moment. @CompuIves , does the extraNodeModules configuration still exist and is documented somewhere?

from metro.

jkillian avatar jkillian commented on May 3, 2024

The solution @willgriffiths and @Strate have posted is confusing to me: why do you have to tell metro to look in the current directory's node_modules directory? Isn't that what it does by default?

And do you have to do this for every single dependency? If so, issues can arise in a workspace setup, because there's no guarantee if a dependency will be installed at the top-level node_modules or in a workspace's node_modules directory

from metro.

mmazzarolo avatar mmazzarolo commented on May 3, 2024

I share the same @brentvatne sentiment on this.
I was able to migrate to the new config format thanks to @rafeca's comment above.

I know that the new config.js format is described in the docs but a quick migration guide would be really helpful 🤔

from metro.

rafeca avatar rafeca commented on May 3, 2024

Regarding the docs, I agree that having some quick migration guide will be helpful. @mmazzarolo , @brentvatne does any of you want to help creating such quick guide? It can be created in the docs folder and will appear in the website.

I can help with clarifications around some parameters if needed 😃

from metro.

dagatsoin avatar dagatsoin commented on May 3, 2024

I am totally lost about this issue :/
Could someone explain me how to import some files which are outside the Expo folder?
For example

- Stories
-- story0.tsx
- MyExpoRoot
-- metro.config.js
-- App.js

How could I import story0.tsx in App.js ?

from metro.

Fallup avatar Fallup commented on May 3, 2024

Here is mine solution combining above approaches which is tested for RN 0.61.2, Typescript, Yarn Workspaces monorepo with React web(CRA) and React Native(without Expo) and shared folders which are not standalone packages (core, theme, ...). It supports absolute paths (I ommited tsconfig.json, you have to specify paths also there apart from babel.config.js)

facebook/react-native#21310 (comment)

from metro.

fabOnReact avatar fabOnReact commented on May 3, 2024

I'm still having problems on a react-native app with absolute imports.

from metro.

brycnguyen avatar brycnguyen commented on May 3, 2024

For expo eas monorepo users:

I had to add:
blacklistRE: exclusionList([/./android/React(Android|Common)/./, /./versioned-react-native/./]),

similar to what is in:
https://github.com/expo/expo/blob/master/packages/expo-yarn-workspaces/index.js

In order to fix the error for Cannot read property 'transformFile' of undefined

using @slorber 's comment

from metro.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.