ES6 Modules, Part 2: Libs .. Wrap ’em up!
Dealing with Legacy Dependencies in a Modular world.
In the last episode, we explored ES6 migration, and the Dual Build strategy: native ES6 Modules and Rollup IIFE bundling, with a side plate of CommonJS, es6 Module Rollup, and Node.
All this is fine for my repo, but what about dependencies?
This story describes a “wrapper” approach to importing legacy libraries.
Issues
In my simulation repo, I have several dependencies.
- Three.js: This turns out to be easy, Three is written as Modules, and has an es-rollup three.module.js. However, it has a plugin architecture that expects THREE to be global. So the nifty 3D navigation is not Modular.
- dat.gui: A wonderfully simple UI perfect for my projects. It too is close to Modular: only 4 edits converted its source to a Rollup-able repo.
- Stats.js: Ubiquitous FPS widget. Already Modular, YaY!
- Pako: Popular in-browser ZIP library. Not Modular. But works as an un-named Import:
import “path/to/module.js”
used for side-effects only.
If at first you don’t succeed..
Initially I wrote individual conversions from the dependencies to a Module format. Sure learned a LOT about their workflow! All were different from one another (hey, that’s what Modules are supposed to solve, so no surprise). Here’s an example: This was the top line of one of the libs, followed by standard JavaScript.
(function(f){if(typeof exports===”object”&&typeof module!==”undefined”){module.exports=f()}else if(typeof define===”function”&&define.amd){define([],f)}else{var g;if(typeof window!==”undefined”){g=window}else if(typeof global!==”undefined”){g=global}else if(typeof self!==”undefined”){g=self}else{g=this}g.pako = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==”function”&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(“Cannot find module ‘“+o+”’”);throw f.code=”MODULE_NOT_FOUND”,f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==”function”&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
WTF?! Using prettier, I was able to figure it out, but still. I want clean code!
What to do? Although some libs were somewhat Modular, there was always a “but” attached. And the code was full of workflow specific things: browserify headers, webpack-specific code, and others. And the task management (recall my repo is npm scripts only) included Gulp, Grunt, WebPack, and Make! And depended at times on being global.
.. Try try again
So after writing several successful “solutions” like these:
- Three plugins: Wrote a simple converter to Module which imported THREE and exported the converter.
- Dat.gui: Wrote a converter from source to Rollup-able form. Note source not in npm, had to
github clone
the repo. - Stats.js: No problem. But is Stats.js the only part of stats.min.js? Nope.
- Pako: Can depend on nameless Module import. For now. But tomorrow?
.. I decided that this was silly so just imported the foo.min.js versions of these as <script>
tags. Sigh. But this seriously complicated my work with others, telling them to include these in their html files along with any future changes in my dependencies.
.. And again
But wait! Those lib.min.js “script” files are just text strings, right? What if I used them inside a tricky wrapper that turned the code into both a global and a Module?
Well there are problems: Modules don’t have a this
. Also, some use the return value of the script. And others set the value in this
expecting it to be the global scope, window
in the browser.
So the gist of the solution is to Wrap a Script in a function called with this
being window and also capture the return value, exporting whichever had the script code.
The Wrapper
The “wrapped” version for Stats.js looks like this, basically:
let returnVal, result
function wrap () {returnVal =
// stats.js - http://github.com/mrdoob/stats.js
(function(f,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"===typeof
<rest of stats.min.js>}
wrap.call(window)
result = window.Stats || returnValexport default result
.. although there are several tweaks like checking if window.Stats already exists, and if the returnVal is a boolean (an uglify weirdness, I think). The wraplib.js node code for constructing the wrappers is here. It takes two args: the library path and its name, and produces an importable wrapped Module:bin/wraplib.js libs/stats.min.js Stats > dist/stats.wrapper.js
There is another, wraplibplus, which does not soil the global space, but it turns out that’s sorta dangerous .. after all the author expected the lib to be global.
Also, wasn’t Stats.js already a Module? Well, yes, but I found through brutal failures to only trust a project’s distributed library .. the code’s workflow can vastly change the source and you want The Real Deal. After all, look at stats.min.js above .. it has a lot that isn’t in the original Stats.js.
Summary
Although not perfect, wrapping is a simple way to convert existing libraries into Modules.
Pros:
- Works on these and several other libraries with no failures.
- Conceptually simple to explain to devs and users.
- Works with all libraries, no fussing with differences and maybes.
- Creates a Modules only repo, including dependencies. No
<script>
‘s.
Cons:
- Kinda weird.
- Still clutters global space. (There’s still wraplibplus.js for that, if needed.)
Let’s remember the key concerns however: I’d like a Modules-only repo, including dependencies, requiring no <script>
tags (a dependency of their own, btw), and a Write and Run developer experience. YaY!
So far, so good!