Like other “serverless” platforms, an OpenWhisk JavaScript Action may be a single .js
file. As functions grow beyond trivial—and begin to depend on third-party modules—that single, forlorn .js
file can no longer shoulder the burden.
The OpenWhisk Documentation suggests simply throwing an entire project—node_modules
and all—into a .zip
file. Of course, that’s wasteful and silly—especially if most of node_modules
is full of development dependencies.
Now, I’m not one to read documentation unless I get stuck, so I didn’t realize that the docs present an alternative: bundle an Action with webpack. Faced with the unpalatable task of zipping up my entire project dir, I knew I wanted to bundle, but I didn’t reach for webpack—I used Rollup.
Why Rollup?
In this article, Rich Harris (the author of Rollup) writes that webpack’s scope is to help bundle single-page applications (SPAs). Rollup, on the other hand:
“Rollup was created for a different reason: to build flat distributables of JavaScript libraries as efficiently as possible, taking advantage of the ingenious design of ES2015 modules.”
—Rich Harris
The key for us in the above quote is “flat distributable.” We want to upload our Action as a single .js
file. That is precisely what Rollup provides—with little extra ornamentation.
Rollup doesn’t do stuff like code splitting nor hot module replacement. We don’t need these features anyway, since we’re not bundling SPAs. Heck, we don’t even need ES modules all the way down (though we won’t get all of the benefits of Rollup’s tree-shaking abilities)—its plugin ecosystem has us covered.
Read on for an example configuration.
An Example Action Using Rollup
Here’s the lovely example action which the OpenWhisk docs provide. It’s intended to be uploaded within .zip
file which also includes node_modules
and everything else in its project folder:
function myAction(args) {
const leftPad = require("left-pad")
const lines = args.lines || [];
return { padded: lines.map(l => leftPad(l, 30, ".")) }
}
exports.main = myAction;
Similarly to OpenWhisk's webpack example, the above needs slight modification to work with Rollup. Let’s write a naïve conversion. Create index.js
:
import leftPad from 'left-pad';
function myAction(args) {
const lines = args.lines || [];
return { padded: lines.map(l => leftPad(l, 30, ".")) }
}
export const main = myAction;
This may seem a little weird, but note that OpenWhisk executes our .js
file as if it were at the top level (no, I’m not sure why). The webpack example in OpenWhisk’s docs make this explicit by assigning the function to global.main
; const main = myAction
is equivalent.
However, since Rollup aggressively tree-shakes, casually assigning myAction
to an unused variable is verboten, and myAction
would be trashed. This is also why we can’t just write export {myAction as main}
; it doesn’t create the global variable converted to CommonJS module format by Rollup. To address this, just export
main
; we can use it later when we write our tests!
Even though our dependencies don’t need to use ES modules, our sources do, so we import
left-pad
at the top. Then, myAction
will become the default export. We’ll see how this works in the Rollup config below.
Need a refresher on ES modules? I recommend the modules chapter of Dr. Axel Rauschmayer’s excellent book, Exploring ES6.
If we don’t yet have a package.json
, we can create one via npm init -y
or copy/paste:
{
"name": "my-action"
}
Then, install rollup
, and left-pad
(assuming npm
v5.0.0 or newer):
$ npm i rollup@^0.57.1 -D
+ rollup@0.57.1
added 56 packages from 109 contributors in 2.725s
$ npm i left-pad@^1.2.0
+ left-pad@1.2.0
added 1 package from 1 contributor in 1.222s
The versions of
rollup
andleft-pad
above, and any subsequent versions, are intended to future-proof this tutorial. We may be able to just use the latest versions of any of these; YMMV.
By default, Rollup looks for a config file in rollup.config.js
, so let’s create that now:
// notice: this is an ES module
export default {
input: 'index.js',
output: {
file: 'dist/my-action.js',
format: 'cjs'
}
};
This configuration declares we will use CommonJS (Node.js-style; require()
and module.exports
, exports
, etc.).
CommonJS (
cjs
) format isn’t actually required by OpenWhisk, as an IIFE or UMD bundle would work, but actually to suppress annoying warnings; if we don’t usecjs
, it will assume we are bundling for a browser and take exception to what we’re trying to do.
Invoke Rollup now, and see the warning it generates:
$ node_modules/.bin/rollup -c
index.js → dist/my-action.js...
(!) Unresolved dependencies
https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency
left-pad (imported by index.js)
created dist/my-action.js in 19ms
In other words, Rollup doesn’t know what to do with left-pad
. In fact, it’s just going to assume it can be retrieved via require()
! If we dump the contents of dist/my-action.js
, we see:
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object')
&& 'default' in ex) ? ex['default'] : ex; }
var leftPad = _interopDefault(require('left-pad'));
function myAction(args) {
const lines = args.lines || [];
return { padded: lines.map(l => leftPad(l, 30, ".")) }
}
const main = myAction;
exports.main = myAction;
Rollup has converted our ES module to Node-style, “CommonJS” exports, which is good. But…
We wanted to bundle left-pad
, yet it didn’t happen; our bundle calls require('left-pad')
like it’s in a node_modules/
somewhere. This won’t do; we only want to upload dist/my-action.js
, and no part of node_modules/
. Where’s the beef?
Other than the fact it broke the internet, left-pad
has two problems:
left-pad
was installed bynpm
and lives innode_modules/left-pad
.left-pad
uses CommonJS exports.
Rollup makes few assumptions about our environment; indeed, both of the above cases necessitate a plugin. Let’s install them now:
$ npm i rollup-plugin-commonjs@^9.1.0 rollup-plugin-node-resolve@^3.3.0 -D
+ rollup-plugin-commonjs@9.1.0
+ rollup-plugin-node-resolve@3.3.0
added 9 packages from 6 contributors in 2.037s
Modify rollup.config.js
as seen here:
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
export default {
input: 'index.js',
output: {
file: 'dist/my-action.js',
format: 'cjs'
},
plugins: [resolve(), commonjs()]
};
If we try bundling again, the warning is gone…
$ node_modules/.bin/rollup -c
index.js → dist/my-action.js...
created dist/my-action.js in 48ms
…and if we view dist/my-action.js
, we will see that the entirety of left-pad
is included. Neat! We can then deploy the action via:
$ wsk action create my-action dist/my-action.js
Or, if we’re using the IBM Cloud CLI (formerly Bluemix CLI):
$ bx wsk action create my-action dist/my-action.js
Next, I’ll discuss a few common recipes I've found useful.
OpenWhisk & Rollup Recipes
Photo by Pratiksha Mohanty / Unsplash
Here are some problems (and solutions, thankfully) I’ve encountered while experimenting with Rollup and OpenWhisk.
Using Built-In Node.js Modules
What if we wanted some extra logging? Node.js’ util.inspect()
is super handy; we can use it to print only a few items from our lines
array:
import leftPad from 'left-pad';
import {inspect} from 'util';
function myAction(args) {
const lines = args.lines || [];
console.log(inspect(lines, { maxArrayLength: 10 }));
return { padded: lines.map(l => leftPad(l, 30, ".")) }
}
export const main = myAction;
Right?
Nope nope nope:
$ node_modules/.bin/rollup -c
index.js → dist/my-action.js...
(!) Unresolved dependencies
https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency
util (imported by index.js)
created dist/my-action.js in 44ms
Fortunately, this is straightforward to fix. We add util
to the external
Array
in rollup.config.js
:
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
export default {
input: 'index.js',
output: {
file: 'dist/my-action.js',
format: 'cjs'
},
plugins: [resolve(), commonjs()],
external: ['util']
};
If I end up using many builtin modules, I like to save myself some trouble and use rollup-plugin-auto-external.
Consuming JSON
What if we want to read a .json
file? We might write this:
import leftPad from 'left-pad';
import {inspect} from 'util';
import {version} from './package.json';
function myAction(args) {
const lines = args.lines || [];
console.log(inspect(lines, { maxArrayLength: 10 }));
return { padded: lines.map(l => leftPad(l, 30, '.')), version };
}
export const main = myAction;
But that would fail!
$ node_modules/.bin/rollup -c
index.js → dist/my-action.js...
[!] Error: Unexpected token
package.json (2:8)
1: {
2: "name": "openwhisk-rollup",
^
3: "version": "1.0.0",
4: "description": "",
Shock! Rollup expects JavaScript files?! The solution is to pull in rollup-plugin-json:
$ npm install rollup-plugin-json@^2.3.0
+ rollup-plugin-json@2.3.0
added 1 package in 1.629s
$ node_modules/.bin/rollup -c
index.js → dist/my-action.js...
created dist/my-action.js in 41ms
Instead of just embedding the entire file, it will grab whatever portion of the .json
file we actually use, effectively tree-shaking the JSON itself. In dist/my-action.js
, we’ll see:
var version = "1.0.0";
Good work, rollup-plugin-json
.
Third-Party Modules in The Environment
IBM provides many commonly used modules in its OpenWhisk service, IBM Cloud Functions. The list of these pre-installed modules which Actions can use is found in the documentation.
Practically speaking, this means we don’t need to bundle these modules. They can be ignored, just like a built-in, as shown above. We add any which we're using to the external
Array
in rollup.config.js
.
It's still helpful to
npm install
any of these which we’re using (for code completion, testing, etc.). They must be added to theexternal
Array
, regardless.
Alternatively, use rollup-plugin-auto-external with option {dependencies: false}
; then add the modules (as minimatch globs) which we do want to bundle to the Array
include
property of the commonjs
plugin’s configuration object. Here’s an example of a Rollup config where we consume the pre-installed request-promise module, but exclude it from the bundle:
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
export default {
// (imagine there is more configuration here)
plugins: [
resolve(),
commonjs({
// left-pad is the ONLY dependency which is bundled
include: ['node_modules/left-pad/**']
}),
autoExternal({
// continue to exclude "util", or any other built-in
builtins: true,
// default is true, but we still must bundle left-pad, so this is false.
dependencies: false
})
],
// request-promise is a dependency in package.json
external: ['request-promise']
}
Smooshing The Bundle
Photo by Sharon Drummond / Flickr
If we follow OpenWhisk's documentation on using webpack to the letter, our resulting bundle will be minified.
In smaller bundles, this ain’t matter. But with larger bundles, we should shave some milliseconds off of startup time.
To minify with Rollup, install rollup-plugin-uglify:
$ npm i rollup-plugin-uglify -D
Then:
import uglify from 'rollup-plugin-uglify';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
export default {
// (imagine there is more configuration here)
plugins: [
resolve(),
commonjs(),
uglify()
]
}
We can squeeze more bytes out of this by providing options to the uglify()
function. By default, it doesn’t mangle top-level variable names. This is how we’d do that:
plugins: [
resolve(),
commonjs(),
uglify({
compress: {
toplevel: true
}
})
]
See the UglifyJS docs for more options.
Obligatory Wrap-Up
Let me remind the reader what the reader read:
- We learned the difference between Rollup and webpack
- We learned how to use Rollup to bundle an OpenWhisk Action
- We learned how to
- Use built-in Node.js modules
- Bundle JSON files
- Consume pre-installed third-party modules
- Minify our bundle
So far, I’ve found Rollup works just as well as webpack for OpenWhisk Action deployment. I don’t see a clear winner, other than they both beat .zip
files. Until I do, I’ll probably continue using Rollup, just ‘cause. What I’d really like to see is a zero-configuration, purpose-built bundler for Node.js OpenWhisk actions. Hmmm…