Watz, a Minimal JavaScript Bundler

tzbitsby
Jason Kantz
Aug 2018
 vol 1.

Motivation

Sometimes it's nice to write small, self-contained programs like a game of life grid, or a graphic for visualizing time, or a little interpreter. To help with this, I wrote watz. It's a short C program with one job--bundle up a dependency tree of JavaScript code into an "executable" HTML file.

The watz command is inspired by C and go. For a C program, I would write some code, build an executable, and run it:

cd $HOME/src/hello
gcc main.c
./a.out

or similarly with go:

cd $HOME/src/hello
go build
./hello

It works well to target web browsers for small programs because of the the convenient HTML, and DOM APIs. The programs are also very portable. I wanted this pattern with minimal installation

overhead, so here's watz for a package called Hello.

cd $HOME/src/hello
watz Hello
chromium index.htm

watz helps avoid having to stitch together the several HTML, JS, CSS files to make things work. It's a way of organizing JavaScript-only programs, and it doesn't have dependencies on jquery, or npm, or browserify, or webpack, or etc.

Installation

git clone https://github.com/jkantz/watz
cd watz
make watz
sudo make install

Inside Hello.js

This is the source code for the Hello scope.

var Hello = watz.Scope("Hello");
Hello.main = function() {
  document.getElementById('webapp').innerHTML += "Hello World";
}

The first line declares the scope. Scopes are 1:1 with JavaScript files, and are declared using "watz.Scope" at the top of each file. The top-level scope for the app requires a main function property that watz will arrange to be called in the generated HTML file.

Hello.js modifies the div element 'webapp', which is the only bit of body HTML generated by watz. The rest of the HTML must be

set up using JavaScript and HTML DOM APIs from within a scope.

Code Organization

Each JavaScript file implements a scope. Scopes are named based on a relative path within the file system. watz will use that path to look for a scope's code in two places--first the current directory, and then in $HOME/webapp.

Give the -w option and a scope name to watz to check "which" file implements the scope. For example, trying this on the sample PrettyPrinter library prints the JavaScript file found under ~/webapp.

$ watz -w github.com/jkantz/watz-apps/PrettyPrinter
~/webapp/github.com/jkantz/watz-apps/PrettyPrinter.js

$ watz -w foo/Bar
no foo/Bar accessible in /home/jkantz/webapp

Putting code under $HOME/webapp is the preferred directory layout. This is similar to The Go Author's (2009) approach to code organization.

  • There is a single workspace, and it's default location is $HOME/webapp.
  • Setting the WATZ_PATH environment variable changes the
  • location of the workspace.
  • The workspace can contain several git repositories, and each git repository contains one or more scopes.

Imports

A scope cannot directly reference identifiers declared in other scopes, instead, it imports references to other scopes. Give the -l option and a scope name to watz to list the scopes that are direct dependencies. For example, the sample HelloWorld from the watz-apps git repository has a single dependency:

$ watz -l github.com/jkantz/watz-apps/HelloWorld
github.com/jkantz/watz-apps/PrettyPrinter

And now that we know more about watz source code organization, we can take a look at what's going on inside:

$ cat "$(watz -w github.com/jkantz/watz-apps/HelloWorld)"
var HelloWorld = watz.Scope("github.com/jkantz/watz-apps/HelloWorld");
// Deps here. No newline between. One newline after last dep.
var PrettyPrinter = watz.Import("github.com/jkantz/watz-apps/PrettyPrinter");

// main is set up by watz to be called when the page opens.
HelloWorld.main = function() {
  PrettyPrinter.println("Hello World");
};

Notice that the PrettyPrinter scope is imported using "watz.Import", and then it's "println" method is used. How did the "println" method get exported?

In the PrettyPrinter source file, the "println" property is set on the declared scope:

$ cat "$(watz -w github.com/jkantz/watz-apps/PrettyPrinter)"
/* It's not that pretty actually. */
var PrettyPrinter = watz.Scope("github.com/jkantz/watz-apps/PrettyPrinter");

PrettyPrinter.println = function(str) {
  document.getElementById("webapp").innerHTML += (str + "<br>");
};

Library files that have no main function can make properties available on the scope, like PrettyPrinter.println above, but in some cases it's useful to set up the scope as a constructor function. For example, "Elt" can be used like this,

var Elt = watz.Import('github.com/jkantz/watz-apps/Elt');
var htm = new Elt('p').child('hello').toString();

This can be done because Elt's scope declaration includes a value--the constructor.

$ cat "$(watz -w github.com/jkantz/watz-apps/Elt)"
watz.Scope("github.com/jkantz/watz-apps/Elt", Elt);

function Elt(name) {
    this.name = name
    this.attributes = {};
    this.children = [];    
}

More Information

Find more examples in the watz-apps git repository (https://github.com/jkantz/watz-apps) and try them out at http://www.kantz.com/watz-apps/.

See the watz man page for the full list of command line options, and please send any questions or comments to <mailto:jason@kantz.com>.

References

Browserling team and Browserify contributors. (2011). Browserify [computer software]. Retrieved from https://github.com/browserify/browserify

Koppers, T., Larkin, S., Ewald, J., Vepsäläinen, J., Kluskens, K., et. al. webpack [computer software]. (2012). Retrieved from https://github.com/webpack/webpack

Mozilla Developer Network. (2018). Web APIs. Retrieved from https://developer.mozilla.org/en-US/docs/Web/API

Schlueter, I. Z., Turner, R., Marchán, K., et. al. (2010). npm [computer software]. Retrieved from https://github.com/npm/cli

The Go Authors. (2009). How to write go code. Retrieved from https://golang.org/doc/code.html

w3schools.com. (2018). JavaScript and HTML DOM Reference. https://www.w3schools.com/jsref/default.asp