jump to content
Source photo by @kadh on Unsplash
Source photo by @kadh on Unsplash

How to Import Modules Out of Webpack's Scope

By

TL;DR: check out the solution

Consider this scenario: you work on a host of projects that all require a set of JavaScript modules, and you want to track and import the common modules from a centralized location. So, you begin to move over these common modules to their own repository; the idea is no more copy-pasting on every change, just import from this new repo!

In this scenario, assume the individual projects use the common workflow of Webpack and Babel to transpile and build .js bundles. Webpack has a great feature that allows aliasing of modules in a given path under a particular identifier, rather than '../path/to/commons/module.js', for example. This also works with modules outside the scope of the Webpack config file; aliasing can be configured under the resolve.alias object:

// webpack.config.js
const path = require("path");

module.exports = {
  // ...
  resolve: {
    alias: {
      commons: path.resolve(__dirname, "../path/to/commons")
    }
  }
  // ...
};

To import these commons modules, the import statement in one of the projects will have to be updated to use this alias:

// index.js
import { getAvgLength } from "commons/stringUtils";

For this scenario, let's say the commons directory has just one file now, the stringUtils.js file imported above; that file looks like this:

// commons/stringUtils.js
export function getAvgLength(...strings) {
  const stringCount = strings.length;
  const combinedStringLengths = strings.reduce((total, str) => {
    return total + str.length;
  }, 0);
  return combinedStringLengths / stringCount;
}

Great utility, right? So the commons directory is intialized with one module, Webpack is aliasing the commons directory in a project directory, and the proper import statement is set up in that project's main JavaScript file. Now, Webpack should be restarted, and a bundle would be successfully built; restarting the browser to get this latest build would show that everything is setup, running smoothly, and no more considerations need to happen.

Except, that's not necessarily the case.

The Problem

What's actually happening is the module being imported from commons isn't actually being loaded by Webpack and transpiled by Babel; it is being imported and placed in the bundle, able to be utilized normally, but that's it. Any modules located outside the scope of the webpack.config.js are being imported and bundled without any additional transformation; this is what a section of the bundle.js would look like:

/***/ "../path/to/commons/stringUtils.js":
/*!*******************************!*\
  !*** ../path/to/commons/stringUtils.js ***!
  \*******************************/
/*! exports provided: getAvgLength */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getAvgLength", function() { return getAvgLength; });
function getAvgLength(...strings) {
  const stringCount = strings.length;
  const combinedStringLengths = strings.reduce((total, str) => {
    return total + str.length;
  }, 0);
  return combinedStringLengths / stringCount;
}

/***/ }),

Unless the commons code is using features that are not supported by browsers yet (or for example, a type annotation system like TypeScript or Flow), no problems will be apparent until a build process is initiated or the code is tested in a browser where some of the commons features are not supported. If the code being imported complies with the target environment's supported ECMAScript version (most likely ES5), then this will likely not be a problem, so this only affects ES2015+ code not in the same directory as webpack.config.js.

The Fix

No amount of tweaks or updates to the project's Webpack or Babel config files will help resolve this issue; the solution is to go to the source. In the commons directory, setup a script that runs all JS files through Babel, which should be run when making changes to the common modules; this can be done by adding a package.json, some form of Babel configuration (seen below as a property in the package.json file), and installing @babel/cli, @babel/core, and @babel/preset-env:

// commons/package.json
{
  "scripts": {
    "babel": "babel entry.js -d dist/"
  },
  "babel": {
    "presets": [
      [
        "@babel/env",
        {
          "targets": {
            "browsers": ["last 2 versions"]
          }
        }
      ]
    ]
  },
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1"
  }
}

When yarn babel/npm run babel is initiated, Babel will transpile all the files matching a glob pattern (in the above example, it only transpiles entry.js) and places the result in dist/. In order for the projects to import the correct, transpiled code, update the Webpack's commons alias to point to that dist directory:

// webpack.config.js
// ...
resolve: {
  alias: {
    commons: path.resolve(__dirname, "../path/to/commons/dist");
  }
}
// ...

When Webpack is restarted or a build is initiated, the bundle should now only output code transpiled down to whatever the Babel settings are setup to output. Problem solved!


I am by no means an expert on the Webpack/Babel workflow, and so I do not know for a fact where in the pipeline the issue lies, but I suspect the breaking point is somewhere in babel-loader, since Webpack is doing its job of importing the modules. Regardless, I did not write this article to point fingers, but to bring awareness and present a solution.

I came across this exact issue while trying to create a common directory for my JavaScript modules at work, and upon running a build, finding that none of the ES2015+ code was being transpiled. Hopefully this helps someone out there thinking of using a similar pattern of development and organization!