evanjon.es

Authoring Micro Libraries with Google Closure Compiler

B roll image footage of library from above.

Requirements.

For the moment this article will assume you’re on a GNU/Linux operating system. In the future I will make changes to ensure you can follow this on MacOS as well. I will not do the same for Windows.

You’ll need to have both Node and NPM installed. Easy enough to do on Ubuntu:

sudo apt install nodejs npm

I also recommend updating NPM via NPM after the previous command has completed.

sudo npm i -g npm@latest

But you shouldn’t use sudo when installing NPM dependencies globally. Follow this to fix.

What we’ll be doing.

We’ll be taking a simple idea for an NPM library (something which fits within one file) and distributing it via NPM after compiling and minifying it with the Google Closure Compiler.

The library will be a simple abstraction over native DOM APIs. For example, when calling .appendChild on an HTMLElement the same HTMLElement will be returned so you’ll be able to chain .appendChild's. The way Element.prototype[someKey] behaves makes doing this incredibly convenient for us – as trying to access certain setters (like textContent) throws errors while attempt to access them, like so:

Element.prototype.textContent; // Uncaught TypeError: Illegal invocation

With this we’ll beable to create proxies for all Element’s prototypes that are functions and create our own “setter” function for all true Element setters.

Here’s the given code of our library below, you can also view this library in full, Ogle TR 122b.

let createElement = tag => {
  let el = document.createElement(tag);

  for (let key in Element.prototype) {
    try {
      let val = Element.prototype[key];
      if (val.call) {
        // We're a function.
        el[key] = function() {
          val.apply(this, arguments);
          return el;
        };
      }
    } catch (_) {
      el["set" + key.charAt(0).toUpperCase() + key.slice(1)] = val => {
        el[key] = val;
        return el;
      };
    }
  }

  return el;
};

The library exports a single function to create a DOM element and wrap all prototype function and setters to make chainable. The simple usage would be:

import createElement from "ogle-tr-122b";

document.body.appendChild(
  createElement("div")
    .setAttribute("class", "container")
    .appendChild(
      createElement("h1")
        .setAttribute("class", "title")
        .setTextContent("Please enter name:")
    )
    .appendChild(
      createElement("input")
        .setAttribute("placeholder", "name")
        .setAttribute("autofocus", true)
        .addEventListener("input", event => {
          let { previousSibling, value } = event.target;
          previousSibling.textContent = `Name: ${value}`;
        })
    )
);

Setup and packages.

Now lets get our package setup. We’ll create a directory, install the necessary packages, and create our NPM scripts.

cd ~/Projects # This is where I store my projects.
mkdir test-gcc # Temp projects I prepend with 'test-'
cd test-gcc # Move into our newly created directory.
echo "{}" > package.json # A _very_ minimal package.json
npm i -D google-closure-compiler-linux # Install our only dev dependency.

Now, lets add the build script to our package.json

{
  "scripts": {
    "start": "node_modules/google-closure-compiler-linux/compiler --language_in=ECMASCRIPT6 --language_out=ECMASCRIPT5 --compilation_level SIMPLE --js_output_file=dist/main.js --js 'src/*.js'"
  },
  "devDependencies": {
    "google-closure-compiler-linux": "^20181210.0.0"
  }
}

What these GCC (Google Closure Compiler) options entail is:

language_in - The language we wrote our library in, we used a modern iteration of ECMAScript (see, let bindings and arrow functions).

language_out - What we’ll be compiling to, ECMAScript 5 (or ES5) will allow this to work in browsers such as IE11.

compilation_level - This will determine the level of code transformations that will take place, if you want to take true advantage of GCC you’ll likely want to be using advanced. Advanced will aggressively rename variable properties, breaking large amounts of code if not properly JSDoc'd. I do not want to cover JSDoc in this article so we’re skipping it (we’ll still have to use some – more on that later).

js_output_file - The final file to be created.

js - The JavaScript files to compile, transform, and minimize.

Exporting and using JSDoc.

Now we have a package.json, a package-lock.json, a node_modules folder which contains our single dev dependency, a src folder which contains our only file (main.js – this contains our createElement function from above), and when we run npm start we’ll obtain a dist folder with a single file of main.js. But running that npm start script won’t do much for us now – because we’re not exporting it correctly per GCC’s rules!

To export a variable, function, whatever, you must include a JSDoc annoation of @export like:

/**
 * @export
 */
let createElement = tag => {
  // ...
};

Now running our npm start will still throw errors, because GCC doesn’t know how to export anything! GCC has a standard library that includes this, we will not use it as it will increase our end size bundle far too much for this simple and small library.

So, lets tell GCC how to export files. To do so we must create a goog namespace that implements a function called exportSymbol. We’ll create this in a file within our src folder called goog.js.

var goog = goog || {};

goog.exportSymbol = function(path, object) {
  if (typeof module !== "undefined") {
    module["exports"] = object;
  } else {
    window[path] = object;
  }
};

This is very similar to a Webpack output. This module will work correctly for both CommonJS + browser script use (the function createElement will be made available on the window object via window.createElement). Not true UMD but close. Implementing true UMD I’ll leave as an exercise for the reader.

Usage.

Great, we’re just about complete with our simple library. Lets run our npm start command and see our newly found main.js file within our dist folder. At this point your project should be very similar to this. Now you’ll need to add a main, version, and name field to the package.json and you’re library is ready for an npm publish.