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.
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.
OK, enough yammering. In order to build a reusable library with TypeScript, we want several things:
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).
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.
To compile the library, we'll process the code in several steps:
tsc
,
targetting ES6.babel
.browserify
.uglify
.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'
}
}
})