Blogs
This article explains how to deploy npm packages bundled with webpack that export and import stuff through the standard ECMAScript modules feature.
This is a sequel of the Moving from AMD to Browser modules in Liferay DXP blog post, so it’s recommended reading that if you haven’t done so already.
Why use webpack with ES modules?
The main goal of using webpack and ES modules together is to be
able to limit the number of JavaScript files needed by an application
while, at the same time, using the standard ES mechanism
(import
and export
) to glue things together.
So for example, think of React, a widely used npm package which currently contains around 40 source JavaScript files. Suppose that you deployed it directly (without webpack transforming it into a bundle) to your server. Every time you loaded your application in a new browser, it would need to download the 40 files.
You could use HTTP/2 to avoid being
hit by the browser connections limit, but even HTTP/2
implementations have their limits, too. Additionally, having
each of React’s JavaScript files available as an ES module wouldn’t
bring any benefit because you would never import anything from them
directly, since you always import React’s API from react
,
not from any internal module. Thus, it makes a lot of sense bundling
the whole React package into a single JavaScript file.
But then, if you bundled it, how would you import anything from it from another module that lives outside of the React package? Continue reading and we will find out how…
How to make webpack expose stuff as standard ES exports
As you probably know, webpack bundles are opaque, in the sense
that a whole application is self-contained in them and nothing is
exported to be consumed from outside. In other words, all your
application’s JavaScript code is contained inside the JavaScript
bundle file that webpack generates and stuff imported/exported (with
standard ES syntax or using require
and
module.exports
) in your application’s source code or
its dependencies, is wired internally with some helpers functions
that webpack puts inside the bundle.
But what if you wanted to generate two different webpack bundles that can import/export things from each other? Or what if you want to deploy your application as separate ES files but, at the same time, use things from another part of your application that has been bundled with webpack?
Cases like the ones described above make sense, for example, in projects that want to deploy their source files directly to the server but still want to use complex packages from npm.
Other projects may want to split the bundles they generate with webpack to avoid downloading a huge bundle at the start which will make the UX worse. For example, think of an application that uses 20 npm packages: it’s very likely that it doesn’t need to download the code for all of them on the first hit of the home page. It would make a lot of sense to break the 20 packages dependency graph in chunks that can be downloaded as needed.
It turns out that you can make webpack re-export the exported symbols of your project’s webpack entry points through ES by configuring it like this:
This configuration will generate a react_bundle.js
file as usual, but instead of being opaque, it will contain some lines
at the end to export react symbols, like this:
What that code is doing is exporting webpack’s internal stuff to the outside world so that it can be consumed by another ES module.
Now, try it yourself and see if it works…
Did it work? Probably not 😈…
Why? Keep on reading…
Anything that can go wrong will go wrong…
If you look at the code webpack is generating you will realize
that it needs to know what react
is exactly exporting
to re-export it in ES syntax. However, that’s not always an easy
task due to the way packages make exports available in Node.js.
If you reflect about how Node.js packages assign stuff to
module.exports
you will soon realize that it may be
impossible to know what a package is really exporting by looking at
the code because you need to execute it to know what is finally put in
that variable.
In the case of react
this is exactly what happens
and you get a bundle file without the ES module exports at the end.
The situation looks bad and we are tempted to fall into despair,
but…, it turns out that there’s a very simple solution to this
problem. We just need to teach webpack how react exports look like.
Let’s see how…
If we create a file named react_desc.js
like this:
And then change webpack’s configuration like this:
Everything will work like a charm, because webpack will be able to infer the things it needs to re-export.
In any case, it’s not necessary that you create a JavaScript file for each package you want to bundle. Some of them are correctly processed by webpack, some others aren’t. A good strategy is trying to use the package directly in the webpack configuration then peek at the insides of the built bundle and see if it correctly exported the package symbols. If that fails, you just create the stub JavaScript file.
How to use our shinny webpacked ESM version of React
OK, so we have a bundled version of webpack that re-exports stuff through ES syntax. Now what? Use it! 🚀
Say you put the generated react_bundle.js
next to
your project’s index.js
file and then you write
something like this in it:
You could now deploy both files to your server (without any need
to process index.js
) and it would render your friendly UI!
How to make webpack use stuff available as standard ES exports
Using the webpack bundle containing React from ES modules is great but what if we want to use it from another webpack bundle?
For example, think of a component library you pull from npm. Or maybe you want to use webpack to bundle your project files together instead of deploying them directly to your server…
For those cases, you can leverage webpack’s externals configuration option. This option allows you to teach webpack how to obtain certain packages from the outside world instead of putting them inside the bundle.
So, say you want to obtain React from the bundle we did before
in your project. All you need to do is write a
webpack.config.js
file like this:
And, after running webpack, you will get a JavaScript bundle that has this code at the very top:
That is the proof that webpack is pulling react from where you told it to get it in the externals configuration, instead of bundling it inside the JavaScript file.
Putting it all together
We will conclude this blog post by showing a very simple example of the things we have explained.
We will use the same file and packages we’ve been talking about and we will bundle react, react-dom and the project with webpack, creating a total of three JavaScript files to be deployed to the server.
package.json
webpack.config.js
index.js
react_desc.js
By using these files, webpack would create three JavaScript
files in the output directory that would be glued together by means
of standard import/export statements. Since all project files and
packages share the same externals
configuration, any
point in code importing react
or react-dom
will pull them from the bundles instead of bundling them internally.
This is also applied to npm packages so if, for example,
react-dom
imported react
, webpack would pull
it from react_bundle.js
too.
Conclusion
We have shown a way to organize your application that lets you do two powerful things:
-
Split your JavaScript dependency graph easily and the way you want by simply defining the bundles you want to build and making them wire together to avoid duplication.
-
Use standard ES syntax to glue things together so that the mechanism is agnostic from the build tool you are using (in this case webpack).
Number one helps a lot with deciding the order and time when you want to load your resources at runtime, so that you don’t incur in big latencies or need a lot of bandwidth. So, for example, you can bundle the things you know will be used for sure in an initial bundle and then put related optional things that may or may not be used in their own bundles.
Also, number one allows you to build your project faster if you
create different projects for each bundle. So, for example, if you are
going to use react
and you know react
doesn’t change unless you upgrade its version, why build it each time
you change your project? Simply create a bundle for it, build it once,
and forget about it until you upgrade it in the future 🎉.
In fact, it turns out that someone has already done it and published it in a public CDN. See the https://esm.sh/ website for more information on this interesting project.
Finally, number 2 lets you interoperate things easily no matter what tool you use to build your project. In other words, you can make two projects cooperate even if one is built with webpack and the other one is plain JavaScript, for example.
I hope this blog post has enlightened you and opened your mind to new ways of structuring your applications. Feel free to leave any comments and/or impressions in the comments section.
Thanks for your feedback!
Bonus track
If you want to see a real example of a Liferay Workspace deploying a Vite application and two import map client extensions to publish you can have a look at this GitHub repository created by Gabriel Prates.