Imports in the Runtime Environment of Webpack 4

Mike Skalandunas
9 min readOct 5, 2020

--

Webpack is an important tool in the current evolution of the JavaScript ecosystem. It does a tremendous amount of work to bundle source code modules into resources that the browser can load and parse. Because users will be interacting with web pages running the generated bundle rather than the source, I think it’s especially important that developers understand how Webpack is manipulating and packaging their code.

🌴 Rumble in the bundle 🌴

I’ve created a contrived application to use as a vehicle for understanding how Webpack handles static ES module imports. The referenced bundles were created with the source found in this repository which uses Webpack 4.44.2. Follow the instructions in the README if you’d like to generate the same bundle.

Now let’s dig in.

The Application

Webpack supports ES modules, CommonJS, and several other module import methods which can be found here, including dynamic import expressions, which are all bundled a little differently from each other. In this case, I’ll be using the ES module syntax, import { fn } from './module;', since it’s the most up-to-date and is also supported by modern browsers/Node.js via the .mjs extension.

Webpack config

The webpack.config.js file used for this project is fairly minimal.

webpack.config.js

Webpack 4 provides a default configuration that helps projects get off the ground quickly. In this case, I’ve only added a mode configuration as well as the html-webpack-plugin.

Providing a mode of 'development' will yield an easily debuggable main.js file that includes some helpful comments from the Webpack team.

The html-webpack-plugin generates an index.html file that’s already referencing the generated Webpack bundle. There are many configuration options available if you want a more robust HTML document, but none were needed for this project.

Source

The entry file contains the bulk of the application logic. It’s the beginnings of a to-do list program, but that’s not important. What’s important are the imports.

src/index.js

L1 (line 1) and L2 contain import statements. The listItemTemplate and complete functions will be available once those statements have been parsed. Because ES modules are unavailable in most browsers, Webpack needs to do some work behind the scenes to expose those exports to the code importing them.

The Bundle

npm run webpack creates a dist directory that contains all of the code exposed in src/index.js as well as the HTML generated by html-webpack-plugin. The dist/main.js file contains Webpack’s runtime code along with the original source.

Source

Before diving into Webpack’s bootstrap, I think it’s worthwhile to see what happened to the source. Scrolling to the bottom of the main.js file reveals an object being passed into the call of an IIFE(Immediately Invoked Function Expression)–the object’s keys are paths to the source files, and the values are functions tasked with encapsulating the source code, which has some obvious additions and alterations.

Source code is stringified, interspersed with Webpack’s dependencies, and passed into an `eval` call

The source code has been stringified, and Webpack’s dependencies have been scattered throughout. When eval is called, the stringified JavaScript will be executed and those Webpack functions will be called as well–exposing the module code to the encapsulating closure. The details of this exposure will be outlined as we parse Webpack’s require function.

Webpack Bootstrap

At the top of the file is the instantiation of the IIFE referenced above, which has been defined by Webpack in a comment as webpackBootstrap. It’s basically the declarations of all the functions and variables Webpack needs to load bundled code, as well as the execution of that code.

The modules argument contains the modified source broken into functions with their corresponding file paths as keys. Next, a number of variables and functions are declared to keep track of and load the various modules and chunks derived from the source.

Webpack bootstrap

Caching & module loading

installedModules is declared and set to an object on the first line of the function body. It’s used as a hash table for caching loaded modules. Any time a module is loaded, its exports are memoized for quick look-ups later by the __webpack_require__ function. This is a performance optimization for situations where a module is imported into multiple files, bypassing a reload, as well as an attempt at parity with the ECMAScript spec. Modules are essentially singletons, meaning only a single instance of the module is referenced once it has been instantiated or in this case loaded.

Entry, dependencies, and sub-dependencies

Let’s say that entry.js imports foo.js on L1 and bar.js on L2. When foo.js is parsed, all exports from utils.js will be loaded and cached in installedModules. Once bar.js is loaded, Webpack will once again attempt to load utils.js. Webpack will check the installedModules cache to see if the module exists, which it will, and then will return the cached exports rather than loading the module again.

Loading static modules

Initial runtime will expose any statically imported modules to the greater execution context. This is done mostly through __webpack_require__, which is declared early in the webpackBootstrap but called on the last line of the function body. Here’s a diagram that outlines critical high-level execution steps for static module loading:

Webpack runtime

Once Webpack’s resources have been loaded and parsed, __webpack_require__ is called with a string representing the path to the main entry point as an argument. This loads the module, loads that module’s static imports, and eventually returns the main entry point.

Webpack’s require function

The __webpack_require__ function is the primary module executor. It is passed into each module as an argument, exposing the evaluated contents to the parent scope. This dependency injection along with stringifying the original source gives Webpack agency to load modules as synchronously or asynchronously as the source demands.

Webpack’s module loading `__webpack_require__` function

Stepping through, the moduleId argument is a string representing a path to a module like we saw earlier in the bootstrap’s modules argument. In the case of this app, the moduleId will be './src/index.js'.

The installedModules cache is checked for the module, and if it exists, execution is stopped and the exports property of that module is returned. If it does not exist, a new module is created (L9) with the moduleId string set to the i property, which is essentially an id. The l, or loaded, property is set to false, and an exports object is created. Here’s how module looks at this point in the execution:

{
i: './src/index.js',
l: false,
exports: {}
}

and here’s installedModules:

{
'./src/index.js': {
i: './src/index.js',
l: false,
exports: {}
}
}

We can see that this particular module still hasn’t been loaded, because the l property is false.

The `modules[moduleId]` function is called, passing in various dependencies

The function at modules[moduleId] is called on L17 with the module.exports object as context, which is currently the empty object instantiated on L14, and with the arguments module, a reference to installedModules[moduleId] aka the module object written out above, and __webpack_require__ itself. These arguments reflect the parameters seen earlier in the modules argument object.

Evaluating a module function

Contextually, we’re still in the require function, but we’ve stepped into the function call where a module is being evaluated. Now that we’re fairly well acquainted with the majority of the function body of __webpack_require__, we should figure out what’s going on in a module when that module is called. I’ll step through this starting on the first line of the eval call, since I’ve already outlined the arguments in the paragraph above.

Module function with formatted stringified file contents

__webpack_require__.r is a function defined in the Webpack runtime that receives an object as an argument, and dresses that object up to look like a CommonJS module as well as an ES Module if the import statement is used.

Next, renderUtils__WEBPACK_IMPORTED_MODULE_0__ is declared, and set to the return value of another __webpack_require__ call, this time with an argument of './src/renderUtils.js'. This will add another entry to installedModules, and run through the require flow with that module as well.

Several lines later, _todo__WEBPACK_IMPORTED_MODULE_1__ is declared, and set to the return value of __webpack_require__('./src/todo.js');, adding yet another entry to installedModules.

At this point, installedModules contains 3 keys–'./src/index.js', './src/renderUtils.js', and './src/todo.js'. Let’s see how it looks.

`installedModules`

The l attribute of '.src/index.js' is still marked as false, because it’s still loading. Its dependencies–renderUtils and todo–have been loaded and their exports are available for use, which is clear from looking at the child properties of exports in the image above.

Once a module has access to its dependencies, the imported functions are able to be accessed and executed.

Object constructor call

Execution may look strange–functions are accessed, passed into Object calls, and then called rather than simply being called as methods.

L18 and L26 see imported functions passed into Object constructor calls and the return value is called with args

This call may seem unnecessary at first since passing a function into the Object constructor yields that same function back, but Webpack wouldn’t include this in the bundle unless it had a purpose.

var foo =
_renderUtils__WEBPACK_IMPORTED_MODULE_0__['listItemTemplate'];
Object(foo)() === foo; // true

According to spec, imported ES modules are in strict mode by default. Functions in strict mode will block access to the window object via the this keyword. Because Webpack does its best to stick to the spec of each of the various import methods, ES modules bundled with Webpack have to behave the same way–this, when referencing the window object in strict mode, should return undefined.

When Webpack exports a module, it exposes an exports object rather than what would natively be something akin to a static binding. Functions that were previously unbound will now be bound to the context of the exports object. Passing these strict functions through Object constructor calls removes that binding.

Logging `this` in strict and not-strict modes, with and without `Object` constructor calls

The rest of the file is evaluated as it was written, resulting in the code being executed.

Stepping back into require

Back in the original require function, L18 sees the module’s l (loaded) property set to true, and then the exports property is returned. At this point, the module object has been updated to look like this:

{
i: './src/index.js',
l: true,
exports: {
Symbol(Symbol.toStringTag): 'Module',
__esModule: true
}
}

The exports module has been updated to represent CommonJS and ES modules. If it had exports, like renderUtils does, it’d include those exports in the exports object as getters.

`listItemTemplate` getter available in the ‘./src/renderUtils.js’ module

That’s about all there is to it. Once __webpack_require__ has exposed all modules, execution ceases and the app has been fully loaded and is idling.

Conclusion

Webpack does its best to be a reliable and performant partner when it comes to executing provided source. In the end there’s no real magic happening here, just plain old JavaScript fundamentals. __webpack_require__ is Webpack’s way to use dependency injection at runtime to expose imports to other scopes. Because file contents are stringified, Webpack can pass in arguments that expose exports in any number of ways. Once the content is evaluated, the application will be running as it was implemented.

Please feel free to contact me with any questions, issues, or clarifications.

Resources

Webpack 4 Analysis repository
https://github.com/mskalandunas/webpack-4-analysis/tree/master/static-imports

Webpack docs
https://webpack.js.org/

Webpack repository
https://github.com/webpack/webpack

TC39 2021 ECMAScript specification
https://tc39.es/ecma262/

MDN
https://developer.mozilla.org/en-US/

Stack Overflow
https://stackoverflow.com/questions/64186011/why-does-webpack-pass-functions-imported-as-es-modules-into-an-object-call-in-we

--

--

Mike Skalandunas
Mike Skalandunas

Written by Mike Skalandunas

Staff Software Engineer @ Hearsay Systems, curious programmer, maker, musician

No responses yet