Published on:



Programming is hard. Web development is hard. Computers are hard. There are a lot of tasks that can get really repetitive, and browser specs change all the time. Luckily, a lot of very smart people have made several tools that we can use to automate many of those tasks.

In my latest Django project I’ve used Gulp, Sass, CSS-Autoprefixer, Browsersync and Bash to automate the crap out of my work flow. This tutorial will show you how to bring these tools together to automate CSS compiling, add vendor prefixes, and automatically refresh multiple browsers every time you edit a file—with one terminal command.

If you’re in a hurry and just want to get stuff done, scroll all the way down for TL;DR copy-paste setup.

Getting Started

This tutorial assumes you already have a normal django project setup. Your project should be contained in a parent directory so that your directory tree looks something like this:

Parent │ ├── project │ ├── [apps] ... │ ├── manage.py │ └── project │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── venv ├ ...

Once you have that, open a terminal in your project directory. You will need to install Node.js by following the appropriate instructions for your operating system. After following those instructions, you can verify that Node and the Node Package Manager (NPM) were installed with $ node --version and $ npm --version respectively.

Next, initialize a node project in this directory with npm init . You will be asked several questions about your project. If you don’t know or haven’t planned what some of them are, the answers are easy to edit later. You should now see a file called package.json in your parent directory. In this file you should see all of the answers you just gave at the command prompt. If any of the information you entered changes in the future, simply update this file. Now for the fun stuff.

Gulp and Django

Gulp is a task runner written in Javascript. It’s an extremely handy tool to combine many tasks into single commands. In order to install Gulp and it’s CLI, run:

npm install -g --save-dev gulp gulp-cli

The --save-dev tells NPM to include gulp and gulp-cli in your packages.json file. The -g option tells NPM to install Gulp globally. This will allow you to use it in any project directory with the command $ gulp [task] . If you don’t include a task argument, Gulp will run whatever you assign to be its default task. Let’s create an example to see how it works.

Create a new file in your parent directory called "gulpfile.js". This is a Javascript file that will tell Gulp what tasks it can do. For starters, put this in gulpfile.js:

var gulp = require('gulp'); // This task can be called from the command line with `gulp django` gulp.task('django', function() { const spawn = require('child_process').spawn; return spawn('python', ['project/manage.py', 'runserver']) .stderr.on('data', (data) => { console.log(`${data}`); }); }); // This task can be called with `gulp` gulp.task('default', ['django']);

The first code block spawns a child process that performs python project/manage.py runserver . Note the inclusion of the project folder in the filepath. If you use Gulp to automate command line tasks, they will all be executed from the same directory as your gulpfile.

The second code block tells Gulp what its default task is. In this case, it runs the gulp task 'django' which we defined above. Gulp’s default task doesn’t have to be a predefined Gulp task. If you prefer, you can include all instructions in your default as a callback function like this:

// This same code block will now run as Gulp's default task. gulp.task('default', function() { const spawn = require('child_process').spawn; return spawn('python', ['project/manage.py', 'runserver']) .stderr.on('data', (data) => { console.log(`${data}`); }); });

While this approach works, it is not preferred. Using seperately defined tasks is easier to maintain, and it allows you to run multiple tasks in parallel (more on that later).

So far this isn't so great. Gulp is only doing one thing. I’m way too lazy to do all the other stuff manually, so let’s automate more things.

Gulp, Sass, and Autoprefixer

It doesn't take long writing vanilla CSS to get tired of not being able to do things like use variables or do math. It takes even less time to get tired of manually adding browser prefixes to CSS properties that aren’t quite universal yet. This is where Sass and Autoprefixer come in. Install the next round of packages with:

npm install --save-dev gulp-sass gulp-concat gulp-sourcemaps gulp-postcss postcss-cli autoprefixer

Gulp-sass will be what process our Sass (or SCSS) into CSS, and Autoprefixer will add vendor prefixes automatically. Postcss-cli is an optional utility that will let you run autoprefixer outside of gulp if you want to experiment to see how it works. Gulp-concat and gulp-sourcemaps will help us in piping information to and from files.

Before wiring up Gulp-sass we need to decide where to put the stylesheets for our project. Ultimately it won't matter where the sass files are, but for the sake of running on the Django development server our CSS files should be located in one of your apps static/ directories.

I am building my project roughly according to the 7-1 pattern described by Hugo Giraudel. If you choose a different project structure it will just require a small change to the example gulpfile.

Boilerplate is on github. After the additon of my sass stylesheets, my directory structure looks like this:

Parent ├── project │ ├── home │ │ ├── static │ │ │ ├ css │ │ │ └ sass │ │ │ ├ main.sass │ │ │ └ ... │ │ └── ... │ ├── [apps] ... │ ├── manage.py │ └── project │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── venv │ ├ ... │ └── gulpfile.js

Now that we have our sass files, let’s add a task to the gulpfile to compile them every time we make a change. Add the following to gulpfile.js:

var sass = require('gulp-sass'); var concat = require('gulp-concat'); // Take gulp.task('styles', function() { gulp.src('./project/home/static/sass/main.sass') .pipe(sass().on('error', sass.logError)) .pipe(concat('style.css')) .pipe(gulp.dest('./project/home/static/css/')); }); // Tell gulp to execute 'styles' every time a sass file changes gulp.task('watch', function() { gulp.watch('./project/home/static/sass/**/*.sass', ['styles']); }); // This task can be called with `gulp` gulp.task('default', ['django', 'watch']);

Running $ gulp styles now will take the contents of main.sass and pipe them through the sass function into a file called style.css located in the directory specified as gulp.dest. $ gulp watch will tell Gulp to watch all sass files in the specified directory and execute the “styles” task every time one of them changes. $ gulp will start the Django server AND start the watch task.

Sass is pretty cool on its own, but using it with autoprefixer is even better. We can autoprefix the output of compiling our CSS with just a slight modification to our “styles” task:

/* other requirements */ var autoprefixer = require('autoprefixer'); var sourcemaps = require('gulp-sourcemaps'); var postcss = require('gulp-postcss'); // Compile main.sass into autoprefixed style.css gulp.task('styles', function() { gulp.src('./project/home/static/sass/main.sass') .pipe(sass().on('error', sass.logError)) .pipe(concat('style.css')) .pipe(sourcemaps.init()) .pipe(postcss([autoprefixer() ])) .pipe(sourcemaps.write('.')) .pipe(gulp.dest('./project/home/static/css/')); });

Now our funciton pipes “style.css” through autoprefixer before writing the final file. Neat!

Gulp and Browsersync

Our last fun toy will reload our project in all open web browsers every time we change a file. That will be a major time-saver over the life of a webapp. Here’s the command to install Browsersync:

npm install --save-dev browsersync

Browsersync includes a command line interface that you can use instead of Gulp, but I found it much easier to include it in my Gulpfile. The options you’d need to use to make Browsersync play nicely with Django make this a task ripe for automation. Let’s see what we need to add to our gulpfile:

/* other requirements */ var browserSync = require('browser-sync').create(); var reload = browserSync.reload; /* other tasks */ // Initiate browsersync and point it at localhost:8000 gulp.task('browsersync', function() { browserSync.init({ notify: true, proxy: "localhost:8000", }); }); gulp.task('watch', function() { gulp.watch('./project/home/static/sass/**/*.sass', ['styles']); // Tell browsersync to reload any time sass, css, html, python, or // javascript files change gulp.watch(['./**/*.{sass,css,html,py,js}'], reload); }); // This task can be called with `gulp` gulp.task('default', ['django', 'browsersync', 'watch']);

The “browsersync” task initiates browsersync and tells it to pay attention to “localhost:8000”. Within the “watch” task, we added a line to watch all Sass, HTML, Python, and Javascript files and reload the browser any time they change. Double neat!

One Bash Script to Rule Them All

Gulp is pretty awesome, and our workflow is pretty darn automated, but in the beginning I promised a single terminal command. As it stands we still have to activate our virtual environment before using Gulp. Let’s fix that. Open a new file in the parent directory called “start-project.bash” (or whatever you want) and add the following:

#!/bin/bash cd [PATH/TO/PARENT/DIRECTORY]; source venv/bin/activate; cd project; python manage.py test; gulp;

And there it is! Now $ ./start-project.bash will do all that other stuff AND run your unit tests on starting up. I included cd statements so that if you want to make this script universally executable it will work from anywhere. That is unfortunately outside the scope of this tutorial as it’s already longer than I thought it would be.

Summary

Today we started with a vanilla Django project, and hooked up a bunch of tools to do some work for us. Now we have the Django server, auto-compiled and prefixed css, and an auto-reloading browser all tied up into one bash script.

If you skipped everything and are here for the TL;DR here it is. All commands should be executed in your project’s parent directory

Install node and initiate node project (non-debian users check here):

curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs node init

Install global node modules:

npm install -g --save-dev gulp gulp-cli

Install local node modules:

npm install --save-dev gulp-sass gulp-concat gulp-sourcemaps gulp-postcss postcss-cli autoprefixer browsersync

Make gulpfile.js in your parent directory:

var gulp = require('gulp'); var sass = require('gulp-sass'); var autoprefixer = require('autoprefixer'); var concat = require('gulp-concat'); var postcss = require('gulp-postcss'); var sourcemaps = require('gulp-sourcemaps'); var browserSync = require('browser-sync').create(); var reload = browserSync.reload; // Run the Django development server gulp.task('django', function() { const spawn = require('child_process').spawn; return spawn('python', ['project/manage.py', 'runserver']) .stderr.on('data', (data) => { console.log(`${data}`); }); }); // Compile main.sass into style.css gulp.task('styles', function() { gulp.src('./project/home/static/sass/main.sass') .pipe(sass().on('error', sass.logError)) .pipe(concat('style.css')) .pipe(sourcemaps.init()) .pipe(postcss([autoprefixer() ])) .pipe(sourcemaps.write('.')) .pipe(gulp.dest('./project/home/static/css/')); }); // Initiate browsersync and point it at localhost:8000 gulp.task('browsersync', function() { browserSync.init({ notify: true, proxy: "localhost:8000", }); }); // Tell gulp to executed 'styles' when sass files change, and execute // a browser reload when any file changes. gulp.task('watch', function() { gulp.watch('./project/home/static/sass/**/*.sass', ['styles']); gulp.watch(['./**/*.{sass,css,html,py,js}'], reload); }); // 'gulp' calls django, browsersync, and watch tasks gulp.task('default', ['django', 'browsersync', 'watch']);

Make bash script start-project.bash in your parent directory:

#!/bin/bash cd [PATH/TO/PARENT/DIRECTORY]; source venv/bin/activate; cd project; python manage.py test; gulp;

Done.