In a JavaScript codebase of sufficient size, you’ll likely run into the need to import or require files from a different part of the codebase. Too much of this can be a sign of a system that isn’t structured very well, but even in a well-architected, modular codebase, there will be dependencies between the modules.

So what do you do when you get tired of typing import MyComponent from '../../../modules/MyModule'?

Part of it depends on the environment and toolset you’re using. Different tools have different options for addressing this problem. You also have to think about how other tools will deal with the solution, such as editors/IDEs, linters, etc.

The answers might be different for libraries than they are for applications. In this post, I’m focused mainly on applications.

There’s a very good summary and subsequent discussion about this issue for Node.js in a Gist by Bran van der Meer.

Here are few options I’ve considered or used.

NODE_PATH

There are a couple of ways of solving the problem with NODE_PATH, and I’ve used that option the past. However, the Node.js module documentation says:

NODE_PATH is still supported, but is less necessary now that the Node.js ecosystem has settled on a convention for locating dependent modules. Sometimes deployments that rely on NODE_PATH show surprising behavior when people are unaware that NODE_PATH must be set. Sometimes a module’s dependencies change, causing a different version (or even a different module) to be loaded as the NODE_PATH is searched.

The Browserify Handbook also talks about this option:

You might see some places talk about using the $NODE_PATH environment variable or opts.paths to add directories for node and browserify to look in to find modules.

Unlike most other platforms, using a shell-style array of path directories with $NODE_PATH is not as favorable in node compared to making effective use of the node_modules directory.

This is because your application is more tightly coupled to a runtime environment configuration so there are more moving parts and your application will only work when your environment is setup correctly.

node and browserify both support but discourage the use of $NODE_PATH.

For those reasons, I’m not such a fan of this option any more.

Put Code in node_modules

The Browserify Handbook has some alternative suggestions, such as putting some of your modules under node_modules, either directly or via symbolic links.

This seems like more hassle than it’s worth. You have to do interesting things with your .gitignore to make it work, and also remember that node_modules doesn’t just contain third-party dependencies.

Break Things Into Modules

If your application already has a modular structure, you could break it into separate npm packages. These could be private packages that you host in a private repository.

Nick Sellen does a great job talking about the various options for this approach.

When an application is under heavy development, it might not be clear where the module boundaries should be yet, so it would be premature to break it up. I prefer a solution that makes it easy to change my mind about where things belong and perform the subsequent refactoring.

Webpack resolve.root

If you’re using Webpack, you can use the resolve.root setting, as described in an article by Grgur Grisogono.

In your webpack configuration file, add:

Webpack Configuration
resolve: {
root: [
path.resolve('./client')
]
}

Using this setting, you can then do import MyComponent from 'modules/MyModule' and webpack will find the file relative to your top-level client directory.

If you also use ESLint and eslint-plugin-import, you can use its eslint-import-resolver-webpack so that ESLint can also find your modules.

In our projects at work, this is our preferred solution and we have it configured in our react-boilerplate project.

React Native

My reason for researching this topic is that I ran into the same import problem in a new React Native project I’m working on. React Native doesn’t use Webpack, so we can’t use its resolve.root setting like we normally do.

React Native allows for a new trick that doesn’t seem to work in the Node.js environment. It turns out that you can place a very minimal package.json file at the root of your source tree:

Minimal package.json
{
"name": "@",
"main": ""
}

With that in place, you can do import MyComponent from '@/modules/MyModule', where @ is the name used in the package.json file.

The React Native package resolver will walk up the directory tree, find your package.json file with the name @, and resolve the import relative to the main entry in that file.

This solution is described more fully by Mike Grabowski.

I ran into a use of this technique in the React Native Katas project.

While this is an interesting technique, it feels like it’s based on an internal implementation detail that may not work in the future.

Babel Plugin

In my research, I also ran into the suggestion to use a Babel plugin for this. If you’re already using Babel, this is a viable option. And if you’re using React Native, you are using Babel.

I saw a number of references to babel-plugin-module-alias. That plugin has since been renamed to babel-plugin-module-resolver.

To use the plugin, you need to add it to your Babel configuration file (either .babelrc or nested inside your package.json file).

Note that React Native has its own Babel configuration. When you add your own Babel configuration to use this plugin, React Native will use it instead of its own configuration, so you have to make sure your configuration has what React Native needs. Fortunately, there is babel-preset-react-native that takes care of the details.

Here is the .babelrc file I’m using on my project:

.babelrc
{
"plugins": [
[
"module-resolver", {
"root": ["./src"]
}
]
],
"presets": ["react-native"]
}

While I haven’t tried this plugin in a non-React Native project, there’s no reason it shouldn’t work elsewhere as well.

For additional tool support, there’s a resolver, eslint-import-resolver-babel-module that works with eslint-plugin-import.

The project README also provides advice about editor integration for Atom and IntelliJ/WebStorm.

Summary

I’ve outlined a number of ways I’ve found to solve the project-relative import problem in JavaScript. I’ve provided a number of links to resources with much more detail than I’ve been able to provide here.

As I mention above, my current approach is to use Webpack’s resolve.root setting for any project using Webpack, and to use babel-plugin-module-resolver for React Native projects.