Building future-proof npm libraries with

rollup.js

Dr. Lukas Taegert
@MNUG, 2017-06-22

And so it happened…
…you have written a
great*
npm library.
*Check out
Maggie Pint @ JSConf EU 2017
Did you wonder
who
might be using your library? E.g.
The question is:
Can you make your library work well
for all
these users?
What users want:

Node Server Developer

  • No useless stuff in node_modules

Node CLI Utility Creator

  • Quickly parsed code

Frontend Developer

  • Code that “runs everywhere”
  • Statically analyzable ES6 exports
Why would they
want
that?

ES6 modules
from a user's perspective

CommonJS ES6
exports.x = 'foo' => export const x = 'foo'
x = require('y').x => import {x} from 'y'
require
can be used anywhere
=> import and export
only allowed at the top level
require(…) permits
dynamic module names
=> import … from enforces
explicit module names

ES6 modules
from a module bundler's perspective

  • Can identify unused modules without executing code
  • Can remove code for unused exports a.k.a.
    tree-shaking
But wouldn't this
break
my library for anyone who doesn't use ES6 imports?
…yes.
How to make ES6 exports work…
package.json:
{
  // ...
  "main": "dist/index.js", // CommonJS version
^1^  "module": "dist/index.mjs", // ES6 version^^
  // ..
}
"module" is automatically recognized
by the major ES6 bundlers
“But I don't want to write my libary
twice”
…and you don't have to:
Just write the ES6 version and use…
rollup.js
  • Bundles ES6 modules into a single file
  • Supports various output formats: ES6,
    CommonJS, AMD, global variable…
  • Optimizes output via
    scope-hoisting and tree-shaking
Basic setup
build.js:
require('rollup')
^1^  .rollup({entry: 'src/index.js'})^^
^2^  .then(bundle => {
    bundle.write({
      dest: 'dist/index.js',
      format: 'cjs' // CommonJS module
    })^^
^3^    bundle.write({
      dest: 'dist/index.mjs',
      format: 'es' // ES6 module
    })^^
^2^  })^^
Then create the bundles with
$ node build.js
Demo
External dependencies
package.json:
{
  // ...
  "dependencies": {^1^"lodash": "*"^^}
  // ..
}
build.js:
require('rollup')
  .rollup({
    entry: 'src/index.js'^3^,^^
^3^    external: ['lodash']^^
  })
  .then(bundle => {/* ... */})
Usually the best way to handle dependencies.
Bundled dependencies
package.json:
{
  // ...
  "devDependencies": {
^1^    "lodash-es": "*",^^
    "rollup": "*"^1^,^^
^1^    "rollup-plugin-node-resolve": "*"^^
  }
  // ..
}
build.js:
require('rollup')
  .rollup({
    entry: 'src/index.js'^3^,^^
^3^    plugins: [
      require('rollup-plugin-node-resolve')()
    ]^^
  })
  .then(bundle => {/* ... */})
When using only little from a library.
Back to the
Demo
But should not tree-shaking
remove
all unused imports?
When tree-shaking
goes wrong
a.k.a “Side-effects in initialization”
All code that is
  • executed during module initialization
  • could potentially have side-effects
will not be removed
Another
Demo
What are side-effects?
  • Modifying a global variable:
    String.prototype.myConst = '42'
  • Calling most global functions:
    const keys = Object.freeze({x: 1})
  • Implicitly calling most global functions:
    '4'.concat('2') // = String.prototype.concat.call('4', '2')
Rule of thumb: Do not execute code upon initialization!
What we have now:

Node Server Developer

  • No useless stuff in node_modules

Node CLI Utility Creator

  • Quickly parsed code

Frontend Developer

  • Code that “runs everywhere”
  • Statically analyzable ES6 exports
Make it run everywhere:
package.json:
{
  // ...
  "devDependencies": {
^1^    "babel-preset-latest": "*",^^
    "rollup": "*"^1^,^^
^1^    "rollup-plugin-babel": "*"^^
  }
  // ..
}
build.js:
require('rollup')
  .rollup({
    entry: 'src/index.js',
    plugins: [
^3^      require('rollup-plugin-babel')({
        presets: [['latest', { es2015: { modules: false } }]]
      })^^
    ]
  })
  .then(bundle => {/* ... */})
Demo
What we have now:

Node Server Developer

  • No useless stuff in node_modules

Node CLI Utility Creator

  • Quickly parsed code

Frontend Developer

  • Code that “runs everywhere”
  • Statically analyzable ES6 exports
Surely
they want all your source and test files in their node_modules folder?
Or specify what they actually need:
package.json:
{
  // ...
  "files": ["dist"],
  // ..
}
Only include files from your"dist"folder
Wrap up
How to future-proof your library:
  • Write your code with ES6 modules
  • Use rollup to create two bundles
  • Distribute them as "main" and "module"
  • Avoid side-effects in initialization!
  • Transpile to ES5 via babel
    (except import/export statements!)
  • Use "files" to specify what to distribute
What next? Automate everything!

Thank you

Slides:

lukastaegert.github.io/rollup-presentation

lukas.taegert@tngtech.com