Angular2 Gulp Typescript Build

Having just started learning Angular this year, I decided to skip v1 and just jump head first into NG2. I had been learning Ember and so the first thing I knew I wanted to get right was my build. Ember uses ember-cli which is pretty amazing. Angular is porting ember-cli to an angular-cli but last I checked, it wasn't quite ready to really use. After doing some more research I settled on Gulp.

There are quite a few articles that will get you up and running with Angular2 and Gulp. The one I used most recently was QUICKSTART: ANGULAR2 WITH TYPESCRIPT AND GULP) because the author had updated it to use the latest RC.1 build of Angular2. There are quite a few similarities between that guide and what I'm posting here, but I think it always helps to see multiple ways of doing something like this.

I decided on a directory structure as follows:

app  
   app.components.ts
   app.html
   main.ts
dist  
   app
   lib
   styles
public  
   styles
      main.scss
   index.html
   systemjs.config.js
gulp.config.js  
gulpfile.js  
package.json  
tsconfig.json  
typings.json  

The app folder is where all my Typescript source code resides along with the templates for the components.

This dist folder is where I will serve the web content from and compile / copy all my assets to. The folder actually gets deleted on a clean and recreated during the build so it is ignored by version control.

The public folder is where my SASS files reside along with some other files that are pretty static and don't need processed or compiled. They just need to wind up in the dist folder.

The rest of the files are your typical gulp and npm standards.

I'm going to list the main files and then I'll talk about some of the finer points. To kick things off, this is the package.json.

{
  "name": "back-office-client",
  "version": "1.0.0",
  "scripts": {
    "clean": "gulp clean",
    "compile": "gulp compile",
    "build": "gulp build",
    "start": "gulp serve",
    "postinstall": "typings install"
  },
  "dependencies": {
    "@angular/common": "2.0.0-rc.1",
    "@angular/compiler": "2.0.0-rc.1",
    "@angular/core": "2.0.0-rc.1",
    "@angular/http": "2.0.0-rc.1",
    "@angular/platform-browser": "2.0.0-rc.1",
    "@angular/platform-browser-dynamic": "2.0.0-rc.1",
    "@angular/router": "2.0.0-rc.1",
    "@angular/router-deprecated": "2.0.0-rc.1",
    "@angular/upgrade": "2.0.0-rc.1",
    "systemjs": "0.19.27",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "0.1.3",
    "rxjs": "5.0.0-beta.6",
    "zone.js": "^0.6.12"
  },
  "devDependencies": {
    "bootstrap": "^3.3.6",
    "bootstrap-sass": "^3.3.6",
    "browser-sync": "^2.12.7",
    "concurrently": "^2.0.0",
    "connect-history-api-fallback": "^1.2.0",
    "del": "^2.2.0",
    "gulp": "^3.9.1",
    "gulp-sass": "^2.3.1",
    "gulp-sourcemaps": "^1.6.0",
    "gulp-tslint": "^4.3.3",
    "gulp-typescript": "^2.12.0",
    "jquery": "^2.2.3",
    "lite-server": "^2.2.0",
    "require-dir": "~0.3.0",
    "run-sequence": "^1.1.5",
    "tslint": "^3.5.0",
    "typescript": "^1.8.10",
    "typings": "^0.8.1"
  }
}

Nothing really crazy there and pretty typical. Next is the tsconfig.json which is the configuration for the Typescript transcompiler.

{
  "compilerOptions": {
    "outDir": "dist/app",
    "target": "es5",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
  ]
}

Next, typings.json.

{
  "ambientDependencies": {
    "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd",
    "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#5c182b9af717f73146399c2485f70f1e2ac0ff2b"
  }
}

Now the systemjs.config.js

(function(global) {

  // map tells the System loader where to look for things
  var map = {
    'app':                        'app', // 'dist',
    'rxjs':                       'lib/rxjs',
    '@angular':                   'lib/@angular'
  };

  // packages tells the System loader how to load when no filename and/or no extension
  var packages = {
    'app':                        { main: 'main.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' }
  };

  var packageNames = [
    '@angular/common',
    '@angular/compiler',
    '@angular/core',
    '@angular/http',
    '@angular/platform-browser',
    '@angular/platform-browser-dynamic',
    '@angular/router',
    '@angular/router-deprecated',
    '@angular/testing',
    '@angular/upgrade',
  ];

  // add package entries for angular packages in the form '@angular/common': { main: 'index.js', defaultExtension: 'js' }
  packageNames.forEach(function(pkgName) {
    packages[pkgName] = { main: 'index.js', defaultExtension: 'js' };
  });

  var config = {
    map: map,
    packages: packages
  }

  // filterSystemConfig - index.html's chance to modify config before we register it.
  if (global.filterSystemConfig) { global.filterSystemConfig(config); }

  System.config(config);

})(this);

There are other module loading systems that can be used. This just seemed like an easy place to start since the Angular2 docs use it as an example.

Now the gulp.config.js.

var historyApiFallback = require('connect-history-api-fallback');

module.exports = function () {  
  var root = '';
  var app = root + 'app/';
  var index = root + 'index.html';

  var build = {
    path: 'dist/',
    app: 'build/app/',
    fonts: 'build/fonts',
    assetPath: 'build/assets/',
    assets: {
      lib: {
        js: 'lib.js',
        css: 'lib.css'
      }
    }
  };

  var bootstrapSass = {
    in: './node_modules/bootstrap-sass'
  };

  var fonts = {
    in: ['app/fonts/*.*', bootstrapSass.in + 'assets/fonts/**/*'],
    out: 'dist/fonts'
  };

  var sass = {
    in: 'public/styles/main.scss',
    out: 'dist/styles/',
    watch: 'public/styles/**/*',
    sassOpts: {
      outputStyle: 'nested',
      precision: 3,
      errLogToConsole: true,
      includePaths: [bootstrapSass.in + '/assets/stylesheets']
    }
  };

  var browserSync = {
    dev: {
      injectChanges: true,
      port: 3000,
      server: {
        baseDir: './dist',
        middleware: [historyApiFallback()]
      }
    },
    prod: {
      port: 3001,
      server: {
        baseDir: './' + build.path,
        middleware: [historyApiFallback()]
      }
    }
  };

  var systemJs = {
    builder: {
      normalize: true,
      minify: true,
      mangle: true,
      globalDefs: { DEBUG: false }
    }
  };

  var config = {
    root: root,
    app: app,
    fonts: fonts,
    bootstrapSass: bootstrapSass,
    sass: sass,
    browserSync: browserSync,
    systemJs: systemJs
  };

  return config;

};

And finally, gulpfile.js

var gulp = require('gulp');  
var runSequence = require('run-sequence');  
var del = require('del');  
var sass = require('gulp-sass');  
var ts = require('gulp-typescript');  
var sourcemaps = require('gulp-sourcemaps');  
var browserSync = require("browser-sync").create();  
var reload = browserSync.reload;  
var config = require('./gulp.config')();  
var tscConfig = require('./tsconfig.json');

// clean the contents of the distribution directory
gulp.task('clean', function () {  
  return del(['dist']);
});

var typingFiles = [  
  'typings/browser.d.ts'
];

gulp.task('serve', ['build'], function () {  
  browserSync.init(config.browserSync.dev);
  gulp.watch([config.sass.watch], ['sass']);
  gulp.watch(['app/**/*'], ['compile', 'copy:assets']).on('change', reload);
  gulp.watch(['public/index.html'], ['copy:index']).on('change', reload);
});

gulp.task('fonts', function () {  
  return gulp
    .src(config.fonts.in)
    .pipe(gulp.dest(config.fonts.out));
});

gulp.task('sass', ['fonts'], function () {  
  return gulp.src(config.sass.in)
    .pipe(sass(config.sass.sassOpts))
    .pipe(gulp.dest(config.sass.out))
    .pipe(browserSync.stream());
});

// copy static assets - i.e. non TypeScript compiled source
gulp.task('copy:assets', function () {  
  return gulp.src(['app/**/*', '!app/**/*.ts'], {base: './'})
    .pipe(gulp.dest('dist'))
});

gulp.task('copy:index', function () {  
  return gulp.src(['public/index.html', 'public/systemjs.config.js'])
    .pipe(gulp.dest('dist'))
});

/// copy dependencies
gulp.task('copy:libs', function () {  
  return gulp.src([
      'node_modules/es6-shim/es6-shim.min.js',
      'node_modules/jquery/dist/jquery.min.js',
      'node_modules/bootstrap/dist/js/bootstrap.min.js',
      'node_modules/zone.js/dist/**',
      'node_modules/reflect-metadata/temp/Reflect.js',
      'node_modules/rxjs/**',
      'node_modules/systemjs/dist/system.src.js',
      'node_modules/@angular/**'
    ], {base: './node_modules'})
    .pipe(gulp.dest('dist/lib'))
});

var typingFiles = [  
  'typings/browser.d.ts'
];

// TypeScript compile
gulp.task('compile', function () {  
  var tsResult = gulp.src('app/**/*.ts')
    .pipe(sourcemaps.init())
    .pipe(ts(tscConfig.compilerOptions));
  return tsResult.js
    .pipe(sourcemaps.write("."))
    .pipe(gulp.dest('dist/app'));
});

gulp.task('build', function(cb) {  
  runSequence('clean', ['compile', 'copy:libs','copy:assets', 'copy:index', 'sass'], cb);
});

gulp.task('default', ['build']);  

There were a few things I wanted for my build process. One was to make sure there weren't a bunch of strange dependencies that didn't make sense in 6 months. Because gulp runs all of it's tasks asynchronously, you can run into some odd recursive dependencies that are just a bit tricky to work around.

So I decided to use the run-sequence plugin to make things easier. You can see it's usage in the build task. I execute the clean task and after it is finished, all the other tasks can take off.

Another big part of what I wanted from my build was a faster browser sync. Most of the articles I found online did the typical build and reload after a change was detected. The problem with this is you might need to do a complete build and so by applying multiple watches and just calling the appropriate tasks, it can really speed things up. You can see this being done in the serve task.

Something new I learned while putting this together was BrowserSync's ability to stream css to the browser so as not to refresh every time. Coupled with the gulp-sass plugin, this is truly awesome and can really improve velocity; especially for designers.

In the sass task you'll see the following:

return gulp.src(config.sass.in)  
    .pipe(sass(config.sass.sassOpts))
    .pipe(gulp.dest(config.sass.out))
    .pipe(browserSync.stream());

Note the last pipe(). So my sass files get compiled and then browser sync streams them to the browser; instant gratification! You can find docs on doing this here.

That's pretty much it. I still need to add a concat and minification process for a more production ready build but for development, this is perfect. If you have any questions, feel free to drop me a comment. I'll do my best to answer them.