flisboac/react-native-monorepo-helper

Error when importing default `index.js` file.

Closed this issue · 7 comments

  • I'm submitting a ...
    [x] bug report
    [ ] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary

There seems to be an error with default index.js file import. When there is a file src/index.js that is reqired like by import src from './index' the bundler throws an error: Module./src does not exist in the Haste module map

  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)

You can replicate the issue with this repo (it was firstly created for some other issue. Sorry for the name confusion).

Can confirm, it seems that custom resolver doesn't handle index files.

Fix in branch #4. From my tests, everything is working. Going to release a new version once you guys give me a positive feedback.

It's a bit odd that resolve can't resolve relative paths when executed in the context of Metro/Haste's module resolution mechanism... Or perhaps I'm doing something stupid (most probable).

Side note: @kaladivo, I even managed to reduce the number of hoisted dependencies in your PoC, e.g.:

// Your root package.json
{
  "name": "monorepo-rn",
  "private": true,
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "workspaces": {
    "packages": [
      "packages/**"
    ],
    "nohoist": [
      "**/react-native"
    ]
  },
  "scripts": {
    "run:web": "yarn workspace web start",
    "run:native:ios": "yarn workspace nativeapp react-native run-ios",
    "run:native:android": "yarn workspace nativeapp react-native run-android"
  }
}

Nice! :)

I'll checkout and try it later today. Just FYI I experimented with a fix yesterday and came up with the following:

Modified the createCustomResolver():

 // After this section
            let resolution = this.resolveInProject(...)
              || this.resolveInProject(...)
              || this.resolveInProject(...)
              || this.resolveInProject(...)
              || null;

// I added the following and index files seem to work. Did not test it extensively.
            // If there is no extension try to load index
            if (!path.extname(moduleName) && !resolution) {
                context.moduleName = `${moduleName}/index`;

                resolution =
                    this.resolveInProject(
                        context,
                        projectRoot,
                        Metro.ResolutionType.SOURCE_FILE,
                        sourceExts
                    ) ||
                    this.resolveInProject(
                        context,
                        monorepoRoot,
                        Metro.ResolutionType.SOURCE_FILE,
                        sourceExts
                    ) ||
                    null;
            }

@meznaric that's a good solution. I did not have the opportunity to check it, but I believe it truly works just by glancing over it. I'd like to add some things, though -- that we should correct.

I just realized that we may not need to call resolveInProject for more than one base directory. We could reduce the number of calls to 2 instead of 4 (or to 3 instead of 6, in the case of your solution) by calling resolveInProject only for the React Native project's root.

Behind the scenes, resolveInProject verifies relative paths (those starting with . and ..) by itself. When the path is not relative, it uses a library called resolve, which implements node's default resolution mechanism.

Relative imports

By node's module resolution algorithm's design, relative-path module imports are always resolved based on the file that imports them (the originModulePath in Haste's context info). We can then avoid making searches in any other places of the filesystem by verifying the existence of the imported file directly from a path derived from originModulePath; otherwise, we could potentially resolve the wrong files.

That's one of the reasons I decided to search for relative paths before using resolve. The other reason is that I could not make resolve.sync() resolve relative paths for some reason, even when providing a proper basedir derived from originModulePath (Haste also randomly provides some really strange originModulePaths like /path/to/dir/., or /path/to/module/index).

Summarizing relative imports, what I mean is that:

  1. When originModulePath is a folder, the resolution's base directory is the originModulePath itself. We can assume this originModulePath is actually an index file (with some unknown extension), but for all intents and purposes, we don't need to know which file it is (e.g. what combination of "index" and extension it has).
  2. If originModulePath is a file, the resolution's base directory must be that file's containing folder, e.g. path.dirname(originModulePath).
  3. We can also receive strange stuff like /path/to/module/index (an incomplete index file path, without an extension, most probably will be the project's index.js), in which case we must consider it to be a file (and therefore follow the same guidelines as the one described in the previous item).

Most of what I just discussed (about relative paths) is already implemented in branch #4. This eliminates the need to make a second resolveInProject call with an index suffix.

Non-relative imports

For non-relative imports, we use resolve.sync() so that we can take advantage of node's standard module resolution. We can avoid using the other monorepo's root directories in this search because Yarn and Lerna automatically set up symlinks to the monorepo modules in the right places; therefore, inter-project imports will just work without any intervention or additional option to the resolve.sync() call, as long as:

  1. The search's base directory is the React Native's project folder; AND
  2. The React Native project is a subfolder of the monorepo's root (which rarely is NOT the case) (I suppose).

Because of that, resolve.sync() is able to hit the correct node_modules folders: the React Native project's and the monorepo root's. It also is able to follow symlinks and resolve them to real paths, unlike Haste (AFAICT).

Some remarks

In my experience, Metro/Haste's distinction between source code and asset doesn't make any difference, we can just use the same resolution strategy for both.


Some of the ideas I discussed here are not fully implemented. After some thinking, I admit I still need to review #4 before making a release.

Another thing I want to do is to improve the Yarn lookup's resolution time when a glob is used. It's taking too much time; perhaps I could use yarn workspaces info --silent instead of globbing/excluding the paths by hand.

If anyone want to review/make suggestions based on #4, please feel free to do so! I'll be more than happy to accept a PR, just notify me so that we don't end up working on the same functionality/fix. (I don't have time to do anything until tomorrow, actually).

I'll checkout and try it later today. Just FYI I experimented with a fix yesterday and came up with the following:

Modified the createCustomResolver():

 // After this section
            let resolution = this.resolveInProject(...)
              || this.resolveInProject(...)
              || this.resolveInProject(...)
              || this.resolveInProject(...)
              || null;

// I added the following and index files seem to work. Did not test it extensively.
            // If there is no extension try to load index
            if (!path.extname(moduleName) && !resolution) {
                context.moduleName = `${moduleName}/index`;

                resolution =
                    this.resolveInProject(
                        context,
                        projectRoot,
                        Metro.ResolutionType.SOURCE_FILE,
                        sourceExts
                    ) ||
                    this.resolveInProject(
                        context,
                        monorepoRoot,
                        Metro.ResolutionType.SOURCE_FILE,
                        sourceExts
                    ) ||
                    null;
            }

I encountered the same problem, and this solution works well! Currently shimming it in with https://www.npmjs.com/package/patch-package. Look forward to using #4!

I tested #4 and it works well for my use case (react-native with react-navigation).

react-native-monorepo-helper v0.2.4 released with the fixes. Please, feel free to comment on the issue if needed. Closing!