ES Modules are the future of JavaScript, but unlike many other es@next features that developers have raced to take advantage of mainly thanks to build tools like babel, working with ES modules alongside of existing NPM modules is a lot harder to get started with.
The purpose of this tutorial is to provide a thorough set of examples for different ways you can approach writing ES modules, without losing interop with the vast library of mostly commonjs modules that exist on NPM today.
Before getting started, it’s important that you have a solid understanding of the differences between the ES module and CommonJS formats. If you haven’t already, please check out Gil Tayar’s excellent blog post before continuing.
Native ES Modules in NodeJS: Status And Future Directions, Part I_ES Modules are coming to NodeJS. They’re actually already here, behind a --experimental-modules flag. I recently gave a…_medium.com
Every approach in this tutorial must satisfy the following requirements:
The functionality of our example NPM module is a bit contrived, but it should touch on all the major pain points, and trust me, there are a lot of them…
Every approach will define an NPM module with a single default export,
async getImageDimensions(input)
, that takes in an image and returns its{ width, height }
.
To show how you can bundle modules with slightly different semantics for Node.js and the browser:
input
as a string
that can either be a local path, http url, or data url.input
as a string
URL or an HTMLImageElement
.Both versions return a Promise
for { width: number, height: number }
.
We’ll start with a naive ES module and work our way through a series of increasingly complex example approaches, all of which are intended to define the same, basic module. You can follow along with the source code for each of the 7 approaches on GitHub here!
Our first approach is the most naive possible use of ES modules supporting our functionality. This approach is broken and provided only as an example starting point. Here is core of the Node.js code:
Approach #1 index.js
Approach #1 load-image.js
The functionality is meant to be pretty straightforward, but it’s important to understand because all the following examples will be using the exact same code. Here is the corresponding browser code:
Approach #1 browser.js
Approach #1 browser-load-image.js
In this approach, we use normal .js
file extensions for es modules and no transpilation.
It is relatively simple and straightforward but breaks several of our module goals:
main
field is an es module whereas it should be a commonjs file.browser
field is an es module whereas it should be a commonjs file.This approach uses babel with babel-preset-env to transpile all Node.js and browser source files into dist/
.
The core of this approach is the build script in package.json:
Approach #2 excerpt from package.json
.mjs
main
and browser
are commonjs exports that support node >= 4
(or whatever we specify in our babel-preset-env config), whereas the module
export is an es module that supports node >=8
due to its usage of async await
.engines
doesn't support specifying that main
supports a certain node version whereas module
supports a different module version, and I'd go so far as to say this is a bad practice.node >= 8
like we did here or add a second babel step which transpiles the node version to an esm folder, although I find this somewhat clunky.This approach uses esm for Node.js and babel+rollup to compile browser source files.
esm is amazing!
Approach #3 main.js node commonjs entrypoint which loads the esm module.mjs via esm.
Approach #3 ollup.config.js for compiling the browser version to bundled commonjs.
.mjs
(only exception is the commonjs entrypoint main.js)
main
module
, and browser
browser
, making debugging the Node.js version easierThis approach uses esm for Node.js and babel+webpack to compile browser source files. It’s the same as the previous approach, except it switches out rollup for webpack.
Approach #4 webpack.config.js for compiling the browser version to bundled commonjs.
.mjs
(only exception is the commonjs entrypoint main.js)
main
module
, and browser
browser
, making debugging the Node.js version easierThis approach uses babel+rollup to compile all Node.js and browser source files. This approach takes the rollup config from approach #3, and takes it one step further by having separate rollup configs for the browser and Node.js targets.
Instead of including the redundant configs, check out the source directly here.
.mjs
main
module
, and browser
node >= 4
(or whatever we specify in our babel-preset-env config) instead of node >= 8
This approach uses babel+webpack to compile all Node.js and browser source files. This approach takes the webpack config from approach #4, and takes it one step further by having separate webpack configs for the browser and Node.js targets.
WARNING: this approach is currently a broken WIP, and its exports do not behave correctly. All other approaches work as intended.
Instead of including the redundant configs, check out the source directly here.
.mjs
main
, module
, and browser
main
and browser
targets are compiled, but the module
target is unable to be compiled due to this issue.node >= 8
, whereas the rollup version is capable of supporting node >= 4
by compiling the module
target as well.This approach uses typescript to transpile all Node.js and browser source files.
TypeScript Approach #7 index.ts
TypeScript Approach #7 load-image.ts
.ts
main
module
, and browser
commonjs
and one targeting esm
require('npm-es-modules-7-typescript').default
. If you know how to prevent this, please let me know.node >= 4
(or whatever we specify in our tsconfig.json
) instead of node >= 8
Overall, using TypeScript feels like the cleanest and most future-proof approach if you want to maximize compatibility.
Whew, that was a lot of JavaScripts…
Now to summarize what I’ve learned from creating this breakdown and my practical suggestions for writing your own next-gen NPM modules:
I hope you’ve found this guide helpful! You can find all the source code, including usable modules for the 7 approaches on GitHub here.
For more info on this topic, check out these great resources:
Have any approaches or suggestions that I left out? Let me know by sharing them in the comments! ❤️
If you liked this article, click the 👏 below, and share it with others so they can enjoy it as well.