In 2020, go bundler-free

ES modules, import maps, snowpack and web_modules…

Damien Seguin
7 min readJan 16, 2020

2020. What a time to be alive. Node.js has officially landed support for ES modules without a flag. The caniuse.com stats on ES6 module support for browser don’t look too shabby with a whopping 88% at the time of writing. A — sort of — new internet exploring browser is released based Chromium, for better or worse. The JS community is starting to stop worrying about JS Fatigue and go with the flow.

Note of past self: “What a time to be alive” oh boy, this intro didn’t age well. On a different note, some of the content below might be relating to an older version of Snowpack. While keeping the same principles, they slightly changed the API and the way you use it. I’d still use it but for ES module package development, I am switching to my own solution, snowdev . In the article linked here, I talk about what snowdev is and also continue the discussion of this present article:

Import maps, a bridge between Node.js and the browser

Two years ago I was writing about the possibilities ES modules in the browser were opening and things have evolved since then.

Self-quote:

Write code like it’s 2020 and you don’t need a compilation/transpilation step anymore (subject to terms conditions or limitations™, keep reading).

To sum up, the main limitation was the ‘bare import specifiers’, meaning you can’t import modules just by a package name eg. no import { partition } from “lodash” or you’ll get the friendly error Relative references must start with either “/”, “./” or “../”.

So how do we solve a decade of writing code in CommonJS and package publishing for consumption in the infamous node_modules folder.

The most promising solution was originally called Package name maps but then became Import maps (Or, how to control the behavior of JavaScript imports). If you are interested in the specification, head this way: https://wicg.github.io/import-maps/.

Otherwise here is the basic idea:

An import map script tag defines mapping between a bare specifier and a URL to be fetched by import statements and import() expressions.

<script type="importmap">
{
"imports": {
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>

The definition above is avoiding you the pain of writing the following:

import { partition } from "/node_modules/lodash-es/lodash.js";

All of this and more can be found in the proposal repo here: https://github.com/WICG/import-maps.

So what does that mean for me, developer writing open source packages and simple applications? I can potentially skip the bundler step in my development process.

I have juggled mainly between the great budō (based on browserify), Gulp, rollup and webpack — in this order. Although they are all important tools solving modern problems, it always involves some complexity in one way or another.

Today, we are striving for simplicity and solutions that make more sense for our different contexts so let’s try that with our new acquired knowledge of import maps.

es-module-shims and node_modules

Import maps are still a proposal so browser support is coming slowly but surely. In the meantime, we can use es-module-shims to go from no support (except Chrome 76 with a flag):

To all major platform support, hurray, thanks one more time to Guy Bedford:

Usage is then pretty simple, you just need to include the script and update script tag types (importmap-shim and module-shim) in an index.html:

<script defer src="es-module-shims.js"></script>
<script type="importmap-shim">
{
"imports": {
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>
<script type="module-shim">
import { partition } from "lodash";
// ...
partition(users, 'active');
</script>

Then simply start a server in the root of your project (where your package.json and node_modules folder are): python3 -m http.server or whichever you prefer. And there you have it:

The same experience you had 10 years ago when manually including all the script tags with src pointing to your vendors folder.

Well… almost. This works because lodash is providing its sources as ES modules (lodash-es) but very often you’ll encounter modules published as CommonJS or UMD only (yes, even React is not published as ES modules yet).

Snowpack and web_modules

How to

Entering the tooling arena, the freshly released Snowpack promises to Build web applications with less tooling and 10x faster iteration.

Note: things have changed with version 2. Most of the following is still valid but some of the api slightly changed.

What now? Yet another bundler? Nope. In its simplest use, npx snowpack , it “re-installs your dependencies as single JS files to a new web_modules/ directory”. Once, on npm install (if you set up a prepare script to run the above command). Not on every code change.

  /* package.json */
"scripts": {
"prepare": "snowpack"
}

You can then use the generated files as ES modules in your application code:

<script type="module">
import React from "web_modules/react.js";
// ...
</script>

An http server is all you need (file:// won’t work), so go with python simple http server or install live-server if you need live-reload.

If all of these /web_modules/ paths makes you cringe or if you want to use TypeScript (which expect bare imports specifiers), you will either have to go the Babel route again or guess what, Snowpack generates imports maps for you: https://www.snowpack.dev/#import-maps. Ultimately your index.html will only need the following:

<script defer src=”web_modules/es-module-shims.js”></script>
<script src=”web_modules/import-map.json” type=”importmap-shim”></script>
<script src=”demo.js” type=”module-shim”></script>

If like me, you are often developing simple modules/prototypes with a visual demo, that’s a pretty convenient method. One gotcha here is that Snowpack will only build web dependencies listed as package.json “dependencies”. So if you are building an npm package and don’t want your demo and its dependencies published alongside your module code, you’ll have to white list some devDependencies or play with the —-include option:

  /* package.json */
"snowpack": {
"webDependencies": [
"three",
"three/examples/jsm/controls/OrbitControls.js",
"dat.gui",
"es-module-shims",
"simplex-noise"
]
}

Regarding performances in the browser, of course caching will be improved but loading time might be impacted if your project grows too big (HTTP/2 is a must but still has limitation). At this point, it will always be time to add a bundler on top of your existing codebase if the —-optimize option is not sufficient.

Another issue I anticipate is my SSD becoming full soon with what is essentially duplicated code between node_modules and web_modules without the npm packages fluff.

Example

Let’s recap with a npm package example: a simple vector field generation and lookup helper in 3D.

The index.js is my main module, exporting a class. To make it work in Node.js (13) I have set the type property of my package.json to module.

But I am more interested in demo-ing it in the browser than Unit Test it. To do so, I have set up Snowpack to get me ES Modules of:

  • three: a 3D rendering library using WebGL
  • its OrbitControls: which actually is a file inside the three package
  • dat.gui: a graphical user interface for changing the speed of my noise field
  • simplex-noise: to generate the noise field (an UMD module)
  • es-module-shims: so I can use the import maps generated by Snowpack

An index.html — as seen before — is importing the shims, import map and main demo module file:

<script defer src="web_modules/es-module-shims.js"></script> 
<script src="web_modules/import-map.json" type="importmap-shim"></script>
<script src="demo.js" type="module-shim"></script>

This demo file imports its dependencies using bare import specifiers. That’s it. The result is visible here: https://dmnsgn.github.io/vector-field/

Bonus: GitHub Pages

So what does it take to have it running on the URL above?

We want to checkout a gh-pages branch, run npm run build to generate optimised modules with npx snowpack --optimise and push web_modules to the repo by removing it from .gitignore.

Fortunately Matt DesLauriers synthesised all of that in a convenient package. I have published a fork https://www.npmjs.com/package/@dmnsgn/ghpages that includes the removal of web_modules from .gitignore on top of bundle.js. Once globally installed, running ghpages -i will do the step above, push and checkout back to master.

Parting words

I’d really enjoy at some point not having to think about the noise that is dependency management in the JS ecosystem. Hopefully solutions like Snowpack and import maps will make the experience of developing for the web even better by merging the best of all JS worlds.

Links:

Notes:

  • es-module-shims can do much more than just helping with the import maps.
  • I have sticked to Node.js and didn’t go into edgy territories like Deno

--

--

Damien Seguin

Tech Lead/Creative Developer @variable_io , Computational Designer, Generative Artisan, Cheese Enthusiast.