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
andrequire
)
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:
-
Transpile the TypeScript source to JavaScript with
tsc
, targetting ES6. -
Transpile the ES6 code output by the TS compiler to vanilla ES5 JS,
using
babel
. -
Bundle the library sources together into a single file for
distribution, using
browserify
. - 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'
}
}
})