loading...

A Nice TypeScript Buildchain

hjfitz profile image Harry ・3 min read

You want to create something awesome in TypeScript, so you set up a nice little directory structure:

A cool project

You want to support older versions of node, so you set up your typescript compiler accordingly:

{
  "compilerOptions": {
    "target": "es5",
    "sourceMap": true,
    "outDir": "dist",
    "moduleResolution": "node"
  },
  "exclude": [
    "node_modules"
  ],
  "files": [
    "src/index.ts"
  ]
}

But wait!

dun dun dunn!

What do you mean I can't use promises? I don't want to import a polyfill, it'd pollute my nice index.ts! If I change to ES6, I get ESM import statements. I can't use those in node!

Enter Gulp and Babel

There's a better way. We can use Gulp. It's a task runner. It runs tasks.

yarn add --dev gulp gulp-babel gulp-jsdoc3 gulp-sourcemaps gulp-typescript babel-preset-env

Note: you can replace yarn add --dev with npm install --save-dev

Now that we've got Gulp, we can take the ES6 output from TypeScript and polyfill that to whatever version we want to support using babel-preset-env.

Here's the part you were probably looking for:

For this, we need to set up two files: gulpfile.js and .babelrc. We'll also amend our tsconfig.json.

// gulpfile.js
const gulp = require('gulp');
const babel = require('gulp-babel');
const sourcemaps = require('gulp-sourcemaps');
const ts = require('gulp-typescript');

const tsProj = ts.createProject('tsconfig.json');

gulp.task('build', () => {
  gulp.src('src/**/*.ts')
    .pipe(sourcemaps.init())
    .pipe(tsProj())
    .pipe(babel())
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest('dist'));
});

gulp.task('default', ['build']);
// .babelrc
{
  "presets": [
    ["babel-preset-env", {
      "targets": {
        "node": "6.10"
      }
    }]
  ]
}
// tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "outDir": "dist",
    "moduleResolution": "node"
  },
  "exclude": [
    "node_modules"
  ],
  "files": [
    "lib/index.ts"
  ]
}

And our final directory structure:

finally!

To build, we simply run: npx gulp, which runs Gulp.

An Explanation

If you were scouring Google for a solution on how to do this, and you've got other stuff to fix, this part isn't for you. If you want to understand what we've just done, stick with me.

Gulp

We use Gulp as the heart of our build. It's a task runner, which means that we can get it to do all sorts of things. Compile SASS, create JSDoc, and even compile TypeScript.

Our Gulp 'build' command does the following:

  • Get all of our TypeScript files: gulp.src('src/**/*.ts')
  • Begin a sourcemap (ideal for debugging in VS Code): .pipe(sourcemaps.init())
  • Compile the TypeScript (Using our tsconfig defined earlier): .pipe(tsProj())
  • Pass the compiled code through Babel: .pipe(babel())
  • Finish our sourcemap: .pipe(sourcemaps.write('.'))
  • Stick out output in 'dist/': .pipe(gulp.dest('dist'));

Babel

We use .pipe(babel()) to run our code through Babel. Babel polyfills. If no arguments are passed, it looks for .babelrc.

Our .babelrc uses babel-preset-env, a fairly new preset for Babel. It's fantastic - all you need to do is provide a version to polyfill* for. More on preset-env here.

*A polyfill, or polyfiller, is a piece of code (or plugin) that provides the technology that you, the developer, expect the browser (read: interpreter) to provide natively - source

npx

npx is a powerful tool that essentially lets you run programs from your node_modules/. Try it with eslint! yarn add eslint && npx eslint --init. It'a sometimes easier if you don't want that binary installed permanently on your system.

I hope this was somewhat informative! It was a total adventure getting this set up for the first time today!

Posted on by:

Discussion

markdown guide
 

Actually, it's even easier - the Typescript compiler has an option for converting import statements to require statements (and export statements to module.exports) built in. You can simply put "module": "CommonJS" into your tsconfig.json file, and you can still stick to using just the Typescript compiler to generate ES6 code that will run in Node.js.

 

That's awesome, I really didn't know that!

I like being able to polyfill though, which was more the point of this. Writing clean async/await code and then compiling to an older version of node is great. I have to support node 6 in my day job.

 

Setting module to CommonJs will compile async await to ES6, the same way Babel does.
Buy this guide is pretty useful considering we can add more functionality with Babel and gulp later on.

 

For Babel 7 (which is still beta) you can use transform-typescript plugin. This way you won't need to use Gulp, just let Babel handle all the transpilation!

 

You could also just add es2015 to tsconfig -> compilerOptions -> lib and set module as mentioned elsewhere in the comments. lib tells tsc "Hey, I know I'm compiling to ES5, but you can count on an ES2015 polyfill." You could restrict the scope further with just es2015.promise, which would tell it that a Promise constructor will be available at runtime.

If you're explicit, you end up with lib being the exact list of polyfills that will be required.

 

It's also worth mentioning, in this setup, you could add your gulp command to the package.json scripts, and then no one needs to know about npx as npm will add ./node_modules/.bin to the path for anything executed with npm run.

e.g.

{
  "scripts": {
    "build": "gulp"
  },
  "devDependencies": {
    "gulp": "..."
  }
}

You'd then be able to run gulp with npm run build or yarn build.

I like using package.json scripts, because it's self-documenting in terms of all of the available build commands.