Getting started with Gulp 4 for Angular

Written by drgenejones | Published 2017/02/08
Tech Story Tags: gulp | javascript | angularjs | grunt

TLDRvia the TL;DR App

Gulp is a build tool used by the Angular team and many other professional engineers. Speed and simplicity are touted as benefits over Grunt. I took another look and attempted to build a simple SPA Angular application using GULP. Here is my story.

Install Gulp

So install GULP — link. And create a gulpfile.js:

var gulp = require('gulp');

gulp.task('default', function() {// place code for your default task here});

Now run it and it should do nothing. Eh Voila:

Genes-MacBook-Pro-2:gulp_plunge thinkjones$ gulp[18:18:56] Using gulpfile ~/dev/GeneConroyJonesArticles/gulp_plunge/gulpfile.js[18:18:56] Starting 'default'...[18:18:56] Finished 'default' after 93 μsGenes-MacBook-Pro-2:gulp_plunge thinkjones$

So far so good.

Directory Structure

So for this app we will be transforming files in the client directory and copying and transforming them over to dist.

# Dir Structure\client\ # Source Files\client\index.html

\dist\ # Dist or converted files.

Copying files with “gulp.dest

Our first GULP task is to copy index.html to the dist folder. To do this we can use gulp.dest. A built in Gulp command.

gulp.task('default', function() {gulp.src('./client/index.html').pipe(gulp.dest('./dist/index.html'))});

Then run gulp

This looks good right? WRONG

What this will actually do is copy items to a folder /dist/index.html/index.html

Gulp.dest needs a folder as a destination. What you really need is:

gulp.task('default', function() {gulp.src('./client/index.html').pipe(gulp.dest('./dist'))});

However if you try to run that next you will get a directory permissions error.

events.js:160throw er; // Unhandled 'error' event^

Error: EISDIR: illegal operation on a directory, open '/Users/thinkjones/dev/GeneConroyJonesArticles/gulp_plunge/dist/index.html'Genes-MacBook-Pro-2:gulp_plunge thinkjones$

This arises from not cleaning the directory before running. A quick Google search later, results in Gulp 4 needing to be installed for gulp.series and del installed via npm. The new gulpfile.js:

var gulp = require('gulp');var del = require('del');

gulp.task('default', gulp.series(clean, copyIndex));

function clean(done) {del(['dist']);done(); # Async callback for completion.}

function copyIndex(done) {return gulp.src('./client/index.html').pipe(gulp.dest('./dist', {overwrite: true}));}

What is nice about GULP is that you can modularise the tasks fairly nicely into functions.

Now running successfully gulp gives us:

Genes-MacBook-Pro-2:gulp_plunge thinkjones$ gulp[18:53:32] Using gulpfile ~/dev/GeneConroyJonesArticles/gulp_plunge/gulpfile.js[18:53:32] Starting 'default'...[18:53:32] Starting 'clean'...[18:53:33] Finished 'clean' after 6.5 ms[18:53:33] Starting 'copyIndex'...[18:53:33] Finished 'copyIndex' after 28 ms[18:53:33] Finished 'default' after 37 ms

Nice output. Let’s check the directory:

Perfect. Let’s add an Angular application in there.

Add Angular App

Bower appears to becoming less favorable it seems unnecessary since we already have npm. Let’s install Angular using NPM.

npm install angular # installs to ./node_modules/angular.js

Let’s refer to that file in our index.html file. We’ll change this later (maybe) but let’s assume we are doing that for now.

<!DOCTYPE html><html><head><title>Gulp Plunge</title>

<!-- Vendor Files -->  
<script src="/node\_modules/angular/angular.js"></script>  

<!-- App Files -->  
<script src="/app/app.js"></script>  

</head><body ng-app="app"></body></html>

Now let’s serve this file and see what it looks like:

npm install --save-dev http-server

We know this will fail because we have added an app.js and have not copied that over to the dist directory. So I added a new copyAppJs function:

var gulp = require('gulp');var del = require('del');

gulp.task('default', gulp.series(clean, copyIndex, copyAppJs));

function clean(done) {del(['dist/**/*.*']);done();}

function copyIndex(done) {return gulp.src('./client/index.html').pipe(gulp.dest('./dist', {overwrite: true}));done();}

function copyAppJs(done) {return gulp.src('./client/**/*.js').pipe(gulp.dest('./dist', {overwrite: true}));done();}

Problem solved after re-running GULP.

But there is still an Angular issue:

Folder node_modules is in the root directory so it will need copying to dist also. Since this involves a vendor code let’s take a look at a potential pattern for adding vendor assets.

Adding vendor assets with GULP.

With large applications you will want to separate vendor code from your application code. The reason being when dev-working your app code will change much more often than vendor code, so in order to maintain the gulp-watch speed, it benefits you not to compile everything, but just the assets within your application. In addition Vendor code is added slightly differently. Whereas you will probably include everything in your app folder you will probably only include certain files from vendor installs. Let’s see how we can manage that process using GULP.

Since every engineer loves DRY let’s specify vendor files in one place, then use them as needed to populate dist/index.html and any other dependent assets.

Vendor source code will be pushed to/dist/vendor:

function copyVendor(done) {var vendor_files = ['./node_modules/angular/angular.js'];return gulp.src(vendor_files).pipe(gulp.dest('./dist/vendor', {overwrite: true}));}

Which copies ‘./node_modules/angular/angular.js’ to /vendor/angular.js

On the surface this looks good but it is missing the package folder. This could lead to conflicts or unintentional overwrites. What we really need is it to copy to:

/dist/vendor/angular/angular.js

To maintain the original directory structure or part of it whilst globbing you do you use the base option

function copyVendor(done) {var vendor_files = ['./node_modules/angular/angular.js'];return gulp.src(vendor_files, {base: './node_modules'}).pipe(gulp.dest('./dist/vendor', {overwrite: true}));done();}

Yay!

Re-running gulp and refreshing the app results in an app which fully works.

However one thing that is frustrating is having to re-run gulp each time. Let’s see how easy it is to get a watch going.

Gulp-watch — Auto update your application code when making changes

So lets add a new function and assign it to a task:

function watchAppJs(done) {return gulp.watch('./client/**/*.*', gulp.series(clean, copyIndex, copyAppJs, copyVendor));}

gulp.task('watch', gulp.series(watchAppJs));

Now when I edit app.js. I see this in the terminal:

[19:42:40] Using gulpfile ~/dev/GeneConroyJonesArticles/gulp_plunge/gulpfile.js[19:42:40] Starting 'watch'...[19:42:40] Starting 'watchAppJs'...[19:42:52] Starting 'clean'...[19:42:52] Finished 'clean' after 4.98 ms[19:42:52] Starting 'copyIndex'...[19:42:52] Finished 'copyIndex' after 45 ms[19:42:52] Starting 'copyAppJs'...[19:42:52] Finished 'copyAppJs' after 6.72 ms[19:42:52] Starting 'copyVendor'...[19:42:52] Finished 'copyVendor' after 20 ms

That was easy.

So what next?

Ensuring index.html contains the latests vendor files.

So what is really annoying me now is that I am maintaining vendor files in index.html:

<head><title>Gulp Plunge</title>

<!-- Vendor Files -->  
<script src="/vendor/angular/angular.js"></script>  

<!-- App Files -->  
<script src="/app/app.js"></script>  

</head>

and gulpfile.js:

function copyVendor(done) {var vendor_files = ['./node_modules/angular/angular.js'];return gulp.src(vendor_files, {base: './node_modules'}).pipe(gulp.dest('./dist/vendor', {overwrite: true}));}

Since gulpfile.js is the source of truth we want to auto populate dist/index.html with the correct dependent assets. gulp-inject Seems like a good candidate. Install:

npm install --save-dev gulp-inject

Then modify the index.html to add template markers to specify where to write assets:

<!-- Vendor Files --><!-- app:js --><!-- endinject -->

Then tell GULP to inject:

var vendor_files = ['./node_modules/angular/angular.js'];

function copyIndex(done) {var sources = gulp.src(vendor_files, {read: false});return gulp.src('./client/index.html').pipe(inject(sources, {name: 'app'})).pipe(gulp.dest('./dist', {overwrite: true}));}

Now dist/index.html looks like this:

<!-- Vendor Files --><!-- app:js --><script src="/node_modules/angular/angular.js"></script><!-- endinject -->

Which is ok but again the files are pointing to node_modules. Our web server is serving them from ./vendor/angular/angular.js. Therefore we need to manipulate the files on the way through. We can do this with the ignorePath and addPrefix options on gulp-inject. ignorePath can strip prefix folders and addPrefix can add the new location in.

Function copy-index now becomes:

function copyIndex(done) {var sources = gulp.src(vendor_files, {read: false});return gulp.src('./client/index.html').pipe(inject(sources, {name: 'app', ignorePath: 'node_modules', addPrefix: 'vendor' })).pipe(gulp.dest('./dist', {overwrite: true}));}

And when gulp is run the vendor files are pointing to the correct dist location:

<head><title>Gulp Plunge</title>

<!-- Vendor Files -->  
<!-- app:js -->  
<script src="/vendor/angular/angular.js"></script>  
<!-- endinject -->  

<!-- App Files -->  
<script src="/app/app.js"></script>  

</head>

The reason I like this approach is because in gulpfile.js we are specifying the actual location of the files:

var vendor_files = ['./node_modules/angular/angular.js'];

And when they are required to exist in a different location they are manipulated as needed.

Code Located Here

There are many other things to look at but so far I like what I see. In subsequent articles we shall take a look at:

  1. LESS compilation
  2. HTML Templating
  3. Minification and Versioning
  4. Automating karma configuration and unit testing.

Published by HackerNoon on 2017/02/08