How to distribute an ES6 library
By library, I mean code designed for other developers to use in a Web app or a Node.js app. These are the variables to optimize:
- Target environment. Node.js only? Web only? or both?
- Legacy support. does it need to support legacy Node.js, legacy browsers, or mobile? (Yea, mobile falls here because ES6 support is still spotty in mobile browsers)
- Consumption (Web only).
<script>
tags,requireJS
, or via some tool like webpack? - Ease of use. How easy is it for developers to consume the library?
- File size. Does it need to be as small as possible?
Target Environment: Node.js
Node.js >= 6
The good news is that most ES6 features work right out of the box, including generators. The other good news is that in Node.js, you don’t usually care about file size since apps don’t usually load dependencies dynamically over the Web.
The bad news is that Node.js doesn’t yet support ES6 modules (import
/export
). If Node.js >= 6 is your only use case, then the simplest workaround is to write ES6 commonJS modules (that is, use ES6 for everything except module creation, where instead you use require
/module.exports
/exports
). The main advantage is that your library will work out of the box (no Babel and no extra user steps).
Alternatively, you can compile to commonJS. See the next case:
Node.js < 6 (or if the library uses an unsupported ES6 feature)
Getting ES6 features in older versions of Node.js means Babel. The good news is that we don’t need to bundle the library into a single file. That means we don’t need to use gulp, rollup, or webpack. The bad news is that handling the external dependencies can be tricky (more on that below). Also, if your users have to write ES6 code in order to consume the library (for example, again, generators), then users will have to transpile their own code as well.
These are the possible external dependencies:
- babel-external-helpers — this is what Babel uses, for example, to create ES6 “classes” (it’s just a wrapper for
class
that converts it to the old school function use). - babel-polyfill — similar to above
- babel-runtime — this replaces babel-external-helpers and babel-polyfill, and as we’ll see, this makes sense for libraries since it allows you to inject the babel dependencies without conflicting with other versions of Babel.
- third party libraries, like lodash
Third-party dependencies can be tricky. You can either:
- bundle the third-party dependency with the library. Unfortunately, the library may pollute the global and therefore not be compatible with other versions of itself. Also, if it’s large, it will significantly increase the library size, which is problematic for Web apps.
- not bundle the third-party dependency. In this case, you need to tell the library’s
package.json
the range of supported versions and Node.js will figure it out. (Except when your library and the app require two incompatible versions of a third-party dependency, and in this case, you’ll need to think about upgrading your library.)
To keep things simple for everyone, not bundling third-party dependencies is the way go. No unwanted file size, no obscure bugs because of conflicts, and you’re using npm in an idiomatic way.
Of course the exception is Babel’s dependencies. These are globals that are known to be incompatible. And if the user isn’t already using these, they require an extra configuration step. So that means the simplest solution is to use babel-runtime to bundle Babel’s dependencies in a non-conflicting way.
Target Environment: Web
Legacy support, <script>
tag consumption
If this is your use case, chances are you also need legacy browser support. That means Babel and babel-runtime for the reasons mentioned above. Legacy browsers also don’t support ES6 modules (import
/export
). That means you’ll need to bundle the library into a single file, preferably in UMD format. That means gulp, rollup, or webpack. You’ll also need to be upfront about external dependencies and have the user add extra <script>
tags to load them.
Note that this use case also covers JSFiddle support, which is good for demos and sandbox environments.
No legacy support, webpack consumption (or equivalent)
If your only use case is modern desktop browsers, this is the absolute easiest case for both you (as the library author) and users. Your library will work out of the box. Webpack and its cousins will do the right thing.
Note that you’ll have to specify the range of supported third-party dependency versions in the library package.json
.
Legacy support, webpack consumption (or equivalent)
This is similar to the Node.js < 6 case.
- You’ll need Babel for transpilation.
- You’ll still need to use babel-runtime to inject the Babel dependencies in a non-conflicting way
- You don’t need to bundle the library to a single file since we’re assuming users use webpack (or equivalent), and these support ES6 modules.
- You’ll still need to specify supported version ranges for third-party dependencies (not Babel).
Note that because you have to inject babel-runtime, the worst case scenario (when you use all helpers and polyfills) is that the library size increases by about 75k.
The alternative is to treat the babel-runtime like a “normal” third party dependency and specify a supported range in the package.json
. Users will then be responsible for a) possibly upgrading Babel or its plugins, and b) injecting babel-runtime in the app.
Multi-Target Environments
What if you have more than one use case? Here’s the feature matrix:
- If you need support for both modern and legacy Node.js, the simplest solution is to build only for legacy Node.js.
- If you need support for both modern Web/webpack and legacy Web/webpack, the simplest solution is to create two builds: one for modern browsers and another for legacy browsers.
- If you need both legacy Node.js and legacy Web/webpack support, then one strategy is to build only for legacy Node.js. Webpack can still consume CommonJS modules. The downside is that you polyfill more features than necessary. The upside is that it’s simpler on end users.
- If you need JSFiddle support, then build a separate UMD bundle. This is also the only use case for a build tool like gulp, rollup, or webpack.
Note that package.json
has support for commonJs modules and ES6 modules. CommonJS modules should use the package.json#main
entry point and ES6 modules should use the package.json#module
entry point.
Now for some code: https://github.com/frankandrobot/sample-es6-library