Building a “front-end build pipeline” from a C# devs perspective - Part 2

In the previous post, we looked at how we can use Gulp to run tasks for us. And in that post we used it to create tasks for transpiling LESS and TypeScript into CSS and JavaScript. But the example was very small and simple. It only contained 1 LESS file and 1 JavaScript file. But what if we have more than 1? Well, that’s when we need to start bundling the files together, and potentially minify them so that they are faster to download. Luckily, this is a piece of cake to do using Gulp. So in this post, we will have a look at how to do that, as well as how to get some TypeScript/JavaScript tests thrown in there as well.

Dislaimer: The solution will still be VERY small and simple. But at least it will be made big enough to be able to use bundling and minification. Which to be honest just means that we need more than one file of each type…

I assume that you have read the last post, and that if you are following along on your machine, you will need to be done with everything that was done in that post…

The first thing that needs to be done to be able to show bundling and minification, is obviously to add more TypeScript files, and more LESS files. However, adding even just one of each is enough to show off the concept. 2 or 2000 files doesn’t matter, the concept is the same. So let’s add one more file of each type. The content, and naming, isn’t really important, but I will call my TypeScript file Animal.ts, and the LESS file for animal-styles.less. And they will look like this on my machine

// animal-styles.less
body {
.animal {
background-color:brown;
}
}


// Animal.ts
class Animal {
constructor(public name) {
}
greet() {
console.log(this.name + ' greets you!');
}
}

As I said, the content doesn’t matter…

Next, let’s modify our gulpfile to include bundling. Something I have chosen to do using a module called “gulp-concat” for the JavaScript, and one called “gulp-concat-css” for the CSS. So after npm installing them, two require statements need to be added at the top, before modifying the “typescript” and “styles” tasks. Once those require statements are in place, all that is needed is to modify the existing tasks by adding “piping” the files to the corresponding bundler like this

// other requires...
var concat = require('gulp-concat');
var concatCss = require('gulp-concat-css');

gulp.task('styles', function() {
return gulp.src('styles/*.less')
.pipe(less())
.pipe(gulp.dest('build/styles'))
.pipe(concatCss("bundle.css"))
.pipe(gulp.dest('build/styles'));
});

gulp.task('typescript', function(){
return gulp.src('src/*.ts')
.pipe(typescript())
.pipe(gulp.dest('build/js/'))
.pipe(concat("scripts.js"))
.pipe(gulp.dest('build/js/'));
});

// other tasks

As the files are bundled, the bundler needs to define a new filename for the resulting data. So this is passed into the concat() and concatCss() functions. These names are then used by the gulp.dest() function when writing the result to disk.

If you run “gulp” now, you will see that next to the transpiled files you now get bundled versions as well.

But this is only bundling. We really want minification as well… As we get large CSS and JavaScript files, we need to get them as small as possible to make the download as fast as possible for the client.

Once again we just install a couple of modules and pipe the data to those. In this case, my choice has fallen on “gulp-uglify” for the JavaScript, and “gulp-minify-css” for the CSS. So it is just a matter of installing those 2 modules using npm, and add them to the gulpfile using a couple of require statements.

However, for this step, I also want to rename the stream I am working with. I want my minified files to have a slightly modified name, compared to the bundles, so that I know that they are minified as well. To do this, I have chosen the “gulp-rename” module. So let’s install that module as well…and add another require for that one as well.

As soon that is done, we can change the bundle tasks to include the minification as well using a couple of extra pipes like this

// other requires...
var rename = require('gulp-rename');
var minifyCSS = require('gulp-minify-css');
var uglify = require('gulp-uglify');

gulp.task('styles', function() {
return gulp.src('styles/*.less')
.pipe(less())
.pipe(gulp.dest('build/styles'))
.pipe(concatCss("bundle.css"))
.pipe(gulp.dest('build/styles'))
.pipe(minifyCSS())
.pipe(rename('styles.min.css'))
.pipe(gulp.dest('dist/'));
});

gulp.task('typescript', function(){
return gulp.src('src/*.ts')
.pipe(typescript())
.pipe(gulp.dest('build/js/'))
.pipe(concat("scripts.js"))
.pipe(gulp.dest('build/js/'))
.pipe(rename('scripts.min.js'))
.pipe(uglify())
.pipe(gulp.dest('dist/'));
});

// other tasks...

As you can see, if you look at the code snippet above, I have added a couple of “pipes” to minify the data, and then renaming the output, before finally writing them to disk in a new directory called “dist”. The files in the “dist” directory are the ones we want to use when we publish our application.

If you run gulp now, you will end up with a “build” directory, containing all the transpiled files, as well as the bundled ones, and a “dist” directory with the bundled and minified version that you want to put into production. And since we have just modified the existing tasks, these new things will automatically be generated by the watchers we put in place in the last post. Easy peasy!

I could have done it all in one swoop, and not saved the steps along the way, but to me, having the transpiled files, and the bundled files available as well, makes it easier to figure out what went wrong is things don’t work. On top of that, it is a lot easier to use the non-bundled files during development. The bundled and minified files are not that great for some things. So in the solution I am working on at the moment, we only use the minified versions in production. During development, we use the unbundled files. And to be honest, you can even serve up the LESS files and have them compiled in the browser if you wanted to. It does slow down the solution a bit though…

You can get to a usable situation with the bundled and minified versions as well using sourcemaps, however, I still need to find a good way to work with thm. So for now, we don’t use them. But if you want to, Gulp can obviously help you to output sourcemaps as well!

Now that we have a fully working pipeline for transpiling and bundling our front-end stuff, we are pretty well set up to development. However, there is one more thing that I want to have in place before then. And that is a way to run JavaScript tests as well. Or rather my TypeScript tests. So let’s see how we go about doing that.

First, we obviously need some tests, and as we are writing our code in TypeScript, we should obviously write our tests in TypeScript as well. But doing so is a bit interesting. Not hard, but interesting.

There are 2 obvious ways of doing it. One simple, and one not so simple. I have chosen the not so simple, as I don’t want to digress into another topic right now, but after the description of what I have done for this “demo”, I will explain briefly how to do it the easier way…

To create tests for the Person class, let’s create a new file in the src directory. Let’s call it Person.tests.ts. Inside it, add a TypeScript reference to the Person.ts file as that is what is about to be tested. Next we need to declare any of the Jasmine based methods we will be using. And if I potentially forgot to tell you that I use Jasmine tests, you have now been told… In this simple case, we will only be using the describe(), it(), beforeEach() and expect() functions. So the declarations look like this

/// <reference path="Person.ts" />

declare function describe(description: string, specDefinitions: () => void): void;
declare function it(expectation: string, assertion?: () => void): void;
declare function beforeEach(action: () => void): void;
declare function expect(actual: any): jasmine.Matchers;
declare module jasmine {
interface Matchers {
toBe(expected: any): boolean;
}
}

Once we got that in place, we can start writing the tests. I won’t talk very much about them as they aren’t important as such. But the existence of them is… So they look like this

// declarations....

describe("Person", () => {

var person: Person;

beforeEach(() => {
person = new Person('Chris','Klug');
});

it("should set fullname property", () => {
expect(person.fullname).toBe("Chris Klug");
});

describe("greet", () => {

it("should log a greeting to the user properly", () => {
var oldLog = console.log;
var loggedEntries = new Array<string>();
console.log = function(str) { loggedEntries.push(str)};
person.greet('Bill')
console.log = oldLog;
expect(loggedEntries[0]).toBe("Greetings Bill, says Chris Klug");
});

});

});

Ok, so now we have some tests to run! However, I did promise to tell you of another way. Well, declaring all of you external dependencies, like we did for the Jasmine functions above, can get a bit tedious, and hard, depending on how many external dependencies you have. Luckily, you can find pre-build TypeScript declarations for most bigger JavaSctipt frameworks. Most of them are available at DefinitelyTyped on GitHub, or through a node module called TSD. Using these pre-defined TypeScript declaration files, you just need to reference them and everything just magically works, instead of having to define every little thing you are working with in you tests…

For JavaScript test running, I have chosen a node module called “testem”. It is a nice “interactive” testrunner that you can set up using a simple json file, and then just start and have running in the background. It will automatically watch your files files for you, serve up the things you want to use to run the tests, and do the testing in whatever browser you wish to use. In my case, I want to run it using PhantomJS, which is a headless browser that doesn’t require you to have a browser window open all the time.

To get started with testem, you need to use your npm skills to install it, as well as PhantomJS if you want to use that. Both can be installed locally, but will work globally as well. The only important thing to get things to work though, is to include the path to the PhantomJS executable location in the PATH environment variable. If you install it locally, setting the path to “node_modules\phantomjs\lib\phantom” works fine. The path can be relative, as long as it is there. If it isn’t, testem fails to start. And it doesn’t give you a good error message, so it can be a bit hard to figure out…

After installing testem, and PhantomJS, we need to set up the configuration. This is done by creating a new JSON-file in the root of the solution, called testem.json. This file will include the configuration needed by testem. For this solution, my testem.json file looks like this

{
"framework": "jasmine2",
"launch_in_dev":["PhantomJS"],
"src_files": [
"src/*.ts"
],
"serve_files": [
"build/js/*.js"
],
"before_tests":"gulp typescript"
}

As you can probably figure out from the above config, testem will run Jasmine2-based tests, and run them in development using the PhantomJS browser. It will also monitor all .ts files in the src directory, and whenever they change, it will re-run the tests. And the tests are run by serving up all the JavaScript files in the build/js directory. However, as I am watching for changes in the TypeScript files, I need to get them transpiled first. Luckily, as I already have this working in my gulpfile, that is just a matter of telling testem to run Gulp’s “typescript” task before running the tests.

Testem has a whole heap of configuration options available, but for this scenario this is enough. If you want to run more browsers, just modify the list in “launch_in_dev” (the available browser options for your machine is retrieved by running “testem launchers”). If you want to watch more, or other, files just modify the “src_files”. And so on… Testem can even be used to run the tests as part of the build pipeline on the build server. However, as we are currently running TFS, we do this using another tool, which I will show later on. So for us, just configuring the “launch_in_dev” is enough. But if you want to run it during builds, you can set that up too.

Ok, that should be it! Typing “testem” and pressing enter in the console should result in something like this

image

And changing any of the TypeScript files should cause an automatic transpile, and then a new test run. It might be a little slow, and can be optimized a bit depending on your solution set-up, but it works very well even in this simple set-up!

That’s it! You now have a fully working “front-end build pipeline” including a nice testrunner that you can have running while you develop your beautiful front-end code. The only problem is that this will now only run on your local machine, and you do NOT want to check in the generated files. They will create merge conflicts on every single check-in. Instead, we want to make sure we run these things on the build server during the build process. But that is a topic for the next post.

Add comment