May 30 2015

Projects A TypeScript workflow with npm and Browserify

Table of Contents

    In re-coding the static site generator responsible for building this site, I decided to use Microsoft's TypeScript as my language of choice, and finally dip my toe into the world of nodejs and the packages available from npm.

    I've spent most of my career as a software engineer on the backend, mostly only working with front-end technologies to create tooling or indulge my interest in data presentation. I finally learned JavaScript and a smattering of modern web development in order to create Tessera, the dashboard system we use for Graphite metrics at Urban Airship.

    I purposely built Tessera without resorting to any of the flavor-of-the-week JavaScript model-view-etcetera... frameworks, reasoning that I wanted to learn JavaScript and its foibles, not just the framework. When Tessera's code base started to get pretty big and hairy, I started thinking about possibly re-writing it in TypeScript, where the benefits of having a compiler and type enforcement at compile time might help keep it in line.

    TypeScript and ECMAScript 6

    With that in mind, I picked TypeScript when I started to re-coded this site while still on vacation in New Zealand. I subsequently re-wrote almost all of Tessera's core code in TypeScript (learning more along the way), and now have a couple of standalone libraries in progress using TypeScript as well (stasher and clusto-client), further refining my workflow (which I now need to bring back full circle, to further updating the code for this site!).

    It turns out that TypeScript has been a really excellent choice, and a lot of that is actually due to using the new features of ECMAScript 6, which goes a very long way to cleaning up many of JavaScript's quirks, and with not too much in the way of build scaffolding its quite feasible to build portable JS libraries that run in any ES5 environment thanks to the magic of transpiling with babel.

    grunt, tsc, babel, browserify

    OK, enough yammering. In order to build a reusable library with TypeScript, we want several things:

    • The ability to write the library across multiple source files
    • Output of a single-file ECMAScript 5 javascript library for maximum compatibility
    • Easy package manager integration (with npm and require)

    The basic steps are actually pretty simple. Write your TypeScript source using TypeScript 1.5 and ES6 modules (i.e. using the import Class from './source' module syntax).

    Handling imports from node_modules

    TypeScript 1.5's implement of import doesn't understand node_modules (not yet, anyway), but handling that is pretty easy - just declare require as an externally defined var.

    declare var require
    const module = require('module')

    Your code then ends up as a mix of import statements for modules local to your project, and require() statements for dependencies.

    Unforunately, most of the external typing files from DefinitelyTyped aren't compatible with TS 1.5 yet.

    Compilation

    To compile the library, we'll process the code in several steps:

    1. Transpile the TypeScript source to JavaScript with tsc, targetting ES6.
    2. Transpile the ES6 code output by the TS compiler to vanilla ES5 JS, using babel.
    3. Bundle the library sources together into a single file for distribution, using browserify.
    4. Finally, minify the source for production use with uglify.

    Gruntfile

    This is a pared-down Grunt config from stasher to illustrate.

    There are, of course, a million different ways to configure the different steps. You can use Browserify as the driver, with typescript and babel both configured as plugins, for example. This setup works well for me though, as I like keeping the phases of compilation separate.

    var SOURCE_FILES = [
      'src/ts/**/*.ts'
    ]
    
    grunt.initConfig({
    
      // 1 - Transpile all TypeScript sources to ES6.
      ts: {
        src: SOURCE_FILES,
        outDir: '_build/es6',
        options: {
          target: 'es6'
        }
      },
    
      // 2 - Transpile the ES6 sources to ES5.
      babel: {
        files: [{ expand: true,
                  cwd: '_build/es6',
                  src: '**/*.js',
                  dest: '_build/es5' }]
      },
    
      // 3 - Bundle the ES5 sources to a single file, exlucding 3rd
      // party dependencies
      browserify: {
        files: {
          '_build/stasher.js' : [ '_build/es5/**/*.js' ]
        },
        options: {
          exclude: [ 'hyperquest', 'querystring', 'URIjs', 'bluebird' ]
        }
      },
    
      // 4 - Minify the bundle for distribution.
      uglify: {
        files: {
          'dist/stasher.min.js' : '_build/stasher.js'
        }
      }
    })