Default export (es5) circ dep

Now that we have basic understand of Webpack's module resolution system, it's time to understand how a circular dependency works.

We use the same four files as on the previous page: entry.js, moduleA.js, moduleB.js and moduleC.js. These are compiled into dist/main.js.

Note: This example is only valid for target 'es5'. We will discuss 'es6' in a later page.

entry.js
moduleA.js
moduleC.js
moduleB.js

_10
import foo from './moduleA';
_10
_10
console.log(foo);

Before continuing, try to figure out what will be console logged by entry.js.

How does the circular dependency look like?

We have an import chain where moduleA.js eventually depends on itself:


_10
entry -> moduleA -> moduleC -> moduleB -> moduleA

And moduleA.js defines a default export, no named exports

How does Webpack handle this circular dependency?

Follow on for a detailed explanation, but in short:

In detail

Full file

Entrypoint requires moduleA.js

The entry point is being executed first. The first thing it encounters is a dependency on moduleA.js.

Webpack_require looks up moduleA.js (1)

There is a cache miss for moduleA.js. Webpack will start the process of looking the module from scratch.

Before any module code is being executed Webpack already sets a PRELIMINARY value for exports for that module in the module cache

This is key in understanding why we do not end up in an infinite loop.

Webpack_require looks up moduleA.js (2)

Imagine we set a breakpoint on line 37, where we are about to call the module function and execute code from moduleA.js

The values of variables is quite interesting!

Without executing a line of code of moduleA.js we have an entry in the module cache!!!

[Dropdown] Values in scope with breakpoint before line 37

_10
moduleId = "moduleA.js";
_10
cachedModule = undefined;
_10
__webpack_module_cache__: {
_10
"moduleA.js": {
_10
exports: {}
_10
}
_10
}

Webpack_require executes the moduleA.js function

Now we carry on with actually executing the moduleA.js code.

The first thing we encounter on line 7 is an import for moduleC.js, which will trigger another call to __webpack_require__.

Similarly, the first line of moduleC will trigger yet another call to __webpack_require__ to resolve moduleB.

Let's fast foward a bit >>

Webpack_require executes the moduleB.js function

So we are now 3 layers of recursion deep:

  1. entry -> __webpack_require__('./src/regular/moduleA.js')
  2. moduleA -> __webpack_require__('./src/regular/moduleC.js')
  3. moduleC -> __webpack_require__('./src/regular/moduleB.js')

Let's add a breakpoint on line 14 and inspect the moduleCache there:

[Dropdown] module cache with breakpoint before line 14

_11
__webpack_module_cache__: {
_11
"moduleA.js": {
_11
exports: {}
_11
},
_11
"moduleC.js": {
_11
exports: {}
_11
},
_11
"moduleB.js": {
_11
exports: {}
_11
}
_11
}

This means that when we call __webpack_require__("./src/regular/moduleA.js") now, we will get a cached result. Even though the value in the cache is not properly set.

This means that the value of 'const bar' on line 15 will become undefined + 'bar' and we can also set the default export to "undefinedbar"

Let's fast forward again >>

Back to the moduleA function execution

We've resolved moduleC.js and moduleB.js and can now continue with the rest of moduleA.js

Let's add a breakpoint on line 8 and inspect the moduleCache there:

[Dropdown] module cache with breakpoint before line 8

_15
__webpack_module_cache__: {
_15
"moduleA.js": {
_15
exports: {}
_15
},
_15
"moduleC.js": {
_15
exports: {
_15
default: "undefinedbarbaz"
_15
}
_15
},
_15
"moduleB.js": {
_15
exports: {
_15
default: "undefinedbar"
_15
}
_15
}
_15
}

This means that the default export of moduleA will become "undefinedbarbaz" + 'bar'

dist/main.js

_58
(function () {
_58
"use strict";
_58
var __webpack_modules__ = ({
_58
"./src/regular/moduleA.js":
_58
(function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
_58
__webpack_require__.r(__webpack_exports__);
_58
var _moduleC__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/regular/moduleC.js");
_58
const foo = _moduleC__WEBPACK_IMPORTED_MODULE_0__["default"] + 'bar';
_58
__webpack_exports__["default"] = (foo);
_58
}),
_58
"./src/regular/moduleB.js":
_58
(function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
_58
__webpack_require__.r(__webpack_exports__);
_58
var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/regular/moduleA.js");
_58
const bar = _moduleA__WEBPACK_IMPORTED_MODULE_0__["default"] + 'bar';
_58
__webpack_exports__["default"] = (bar);
_58
}),
_58
"./src/regular/moduleC.js":
_58
(function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
_58
__webpack_require__.r(__webpack_exports__);
_58
var _moduleB__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/regular/moduleB.js");
_58
const baz = _moduleB__WEBPACK_IMPORTED_MODULE_0__["default"] + 'baz';
_58
__webpack_exports__["default"] = (baz);
_58
})
_58
});
_58
_58
var __webpack_module_cache__ = {};
_58
_58
function __webpack_require__(moduleId) {
_58
var cachedModule = __webpack_module_cache__[moduleId];
_58
if (cachedModule !== undefined) {
_58
return cachedModule.exports;
_58
}
_58
var module = __webpack_module_cache__[moduleId] = {
_58
exports: {}
_58
};
_58
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
_58
return module.exports;
_58
}
_58
_58
!function () {
_58
__webpack_require__.r = function (exports) {
_58
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
_58
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
_58
}
_58
Object.defineProperty(exports, '__esModule', { value: true });
_58
};
_58
}();
_58
_58
var __webpack_exports__ = {};
_58
_58
!function () {
_58
__webpack_require__.r(__webpack_exports__);
_58
var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/regular/moduleA.js");
_58
console.log(_moduleA__WEBPACK_IMPORTED_MODULE_0__["default"]);
_58
}();
_58
})()
_58
;

Lastly, after all imports are required, it means that the module cache will become:


_17
__webpack_module_cache__: {
_17
"moduleA.js": {
_17
exports: {
_17
default: "undefinedbarbazbar"
_17
}
_17
},
_17
"moduleC.js": {
_17
exports: {
_17
default: "undefinedbarbaz"
_17
}
_17
},
_17
"moduleB.js": {
_17
exports: {
_17
default: "undefinedbar"
_17
}
_17
}
_17
}

And the entrypoint will console.log 'undefinedbarbazbar'.

Take aways

  1. Webpack will avoid an infinite loop by setting preliminary module.exports = {} for each module.
  2. When the module which is depending on itself uses a default export, it will cause the dependent version to be undefined

Next example

Click here to visit the next example with named exports