So you want to write a library? Oh, and you want to leverage some ultimate-mode TC39 syntax? AND you want to make your code consumable in a variety of module formats? Well, friend... a few things.
Most front-end developers know that we can’t just write JavaScript using the latest features and expect it to work across any environment. That’s why we use tools like babeljs to transpile our source code into ye olde JavaScript of 200X. Good on ya’.
When we’re writing libraries, we have the problem of not knowing what module format will acceptable to applications that use our library. Additionally, we need to consider how our library will impact bundle sizes.
That being the case, I like to build to multiple formats if I’m going to distribute my library on npm. This isn’t the easiest thing to do, and there’s a few ways to skin it, but it’s not too bad with a little boiler-plate and tools like rollup and Webpack.
webpack
or rollup
If we’re writing a library, deciding is easy. Use rollup
. I don’t know of a
way to transpile to esm
using webpack
. However, I’m still including
examples of webpack
configurations, as I think it exposes a pattern that can
be super useful, namely extensible configuration. I’ll likely write about that
pattern in the future.
Goals
- Deliver a library in multiple module formats (umd/cjs/esm)
- Deliver a library that is transpiled, regardless of module format
- Keep our library size down by omitting dependencies
- Communicate module formats to folks that use the library
Extensible configuration
Both webpack
and rollup
are configurable to build to different module
formats. Whichever we choose, we’ll want to create a base configuration that
can be extended with information about what module format to target.
// base.js
// Both tools require a config object that has an `entry`
export default {
entry: 'src/index.js',
// etc... This will differ based on what build tool we choose
}
Now, we need to write configurations for multiple module formats by extending
our base configuration. To do this, we use the trusty Object.assign
. Notice
that both of these examples export arrays.
// webpack.config.js
const baseConfig = require('./base.js')
// umd
const umd = Object.assign(
{},
baseConfig,
{
output: {
filename: '[name].js',
libraryTarget: 'umd'
path: path.resolve(__dirname, 'dist/umd')
}
)
// cjs
const cjs = Object.assign(
{},
baseConfig,
{
output: {
filename: '[name].js',
libraryTarget: 'commonjs',
path: path.resolve(__dirname, 'dist/cjs')
}
)
// yep yep
module.exports = [ cjs, umd ]
// rollup.config.js
import base form './base.js'
const esm = Object.assign(
{},
base,
{
output: {
file: 'dist/esm/index.js',
format: 'es'
}
}
)
const cjs = Object.assign(
{},
base,
{
output: {
file: 'dist/cjs/index.js',
format: 'cjs'
}
}
)
export default [ cjs, esm]
Omit dependencies
Bundle sizes… yeesh. webpack
and rollup
both have the ability to exclude
module dependencies from the bundles that are generated, a feature that’s very
useful for libraries. By using it, we’ll reduce duplicate module code in
applications that consume our library.
// rollup
const rollupBase = {
external: [
'debug',
'rxjs/Observable',
'rxjs/Rx',
'rxjs/add/observable/fromEvent',
'rxjs/add/operator/take'
]
}
// webpack
const webpackBase = {
externals: {
'debug': 'debug',
'rxjs/Rx': {
commonjs: 'rxjs/Rx',
commonjs2: 'rxjs/Rx',
amd: 'Rx',
root: 'Rx'
}
}
}
Build it.
Now we just need a build script in package.json
, and we can generate our
distributable modules with npm run build
.
// package.json
{
"scripts": {
"build": "npx rollup -c rollup.config.js"
}
}
Communicate module options
Now that we have a library that delivers a few different formats, we need to
make sure our package.json
points at things correctly.
main
absolutely has to point at a cjs
or umd
format. Otherwise, we’ll be
sort of breaking faith with the wider npm
ecosystem.
Some build tools know to use module
instead of main
when importing modules.
We’ll point module
at our esm
code, which is explicitly not our source
code. Why? Well, we have no guarantee that folks consuming our library will
have the appropriate transforms in their build pipeline, but we can still
deliver tree-shakeable code, which once again helps reduce the bundle-size cost
of using our library.
// package.json
{
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js"
}
Finally, good documentation about the various module formats is welcome. For
instance, a umd
build might not be exposed at all in package.json
, but
letting folks know where to get it is a nice-person move.
<!-- README.md -->
# Which formats?
Blah blah blah.
If you want the `umd` build, grab it from `module/dist/umd`.
Real life
If you want to see a real life example of this, check out my
observable-socket
library. At the time I refactored it to deliver better module formats, I don’t
believe rollup
had the ability to receive an array of configurations, so the
build script is quite a bit different.