Recently, we have moved our whole deployment script from raw bash scripts to Grunt.js. We needed a bulletproof solution that lets us focus on product development, not its maintenance. Within a few days of work, the results were really fascinating.
The problem: custom deployment scripts
In LiveChat, as in other web products, some common deployment patterns need to be followed:
- Turn many JavaScript files into a single
.js
file. - Turn many CSS (or SASS/LESS) files into a single
.css
stylesheet. - Compress your JS/CSS code to reduce file sizes.
- Append MD5 fingerprint to file names to take advantage of heavy browser caching.
- Create image sprites to limit the number of HTTP requests.
- Send the files to the server(s).
We used to use bash scripts for doing this donkey work. But when you see this over and over…
if [ `wc -c "$BASEDIR/app.js" | awk '{print $1}'` -lt 50 ]; then
echo "File size lower than 50 chars. Aborting."
exit
fi
if [ $(tail -c7 "$BASEDIR/app.js") != '/*OK*/' ]; then
echo "File does not end with \"/*OK*/\" comment. Aborting."
exit
fi
…and again and again…
function putParamIntoFile {
sed "s/$2/$3/g" $1 > "$1.tmp"
mv "$1.tmp" $1
}
putParamIntoFile "$SCRIPTS_PATH/index.html" "{{jsPath}}" "${CDN_TARGET_DIR}app.$DATE.js"
…I can go on…
echo "** Adding timestamp to file..."
echo "/* `date` */" > $BASEDIR/date.tmp
mv $BASEDIR/tracking.js $BASEDIR/tracking.date.js
cat $BASEDIR/date.tmp $BASEDIR/tracking.date.js > $BASEDIR/tracking.js
rm $BASEDIR/date.tmp $BASEDIR/tracking.date.js
…that’s when you’re starting to realize that there must be a more humane solution.
The solution: Grunt.js
Grunt’s popularity speaks for itself. Not even a year has passed since its creation and Grunt can now perform more than 1200 (!) different tasks such as minification, code validation, files concatenation, sprites generation and more. 4 new plugins for Grunt.js are published every day. Now, that’s a good pace of growth.
LiveChat has two big projects that need to be deployed on a daily basis: a chat widget (product used by website visitors) and the web application (used by agents talking with these visitors).
1. The chat widget — deployment
A chat widget (the one you can see in the bottom-right corner of this page) is installed on our customers’ websites. It’s written in pure JavaScript. Grunt.js automates the following deployment tasks for us:
- copy — copies some files from one place to another.
- less — compiles LESS files to CSS.
- cssmin — minifies CSS files.
- concat — concatenates multiple source files into a single one.
- replace — replaces inline patterns with variables. I believe most advanced projects involve some kind of regexp replacement, and it’s a great tool to do this.
- glue — helps us build image sprites using a great Glue library.
- uglify — minifies files before we deploy them to our customers.
- shell — helps us run an SCP command which sends the final version of the chat widget code to our servers.
- watch — re-compiles the chat widget source whenever we save a new version of the source file in a text editor. Immensely useful during local development.
Here’s the good part: we have a single tool for both building local versions and deploying the product to our customers. When we want to run a development version of the widget, including watch
task for instant re-compilation, we just use:
grunt
Deploying the widget to our staging environment could not be simpler:
grunt deploy:staging
What if a chat widget version is ready to be deployed to all customers?
grunt deploy:production
But there’s one more thing. The learning curve for other team members became significantly shorter, compared to our previous solutions. If a web developer worked with Grunt before, he can start working on the project immediately by simply typing in the grunt
command.
2. The web application — deployment
Our customers sign in to the web application to handle chats with their customers. The front-end of the application is written in CoffeeScript, HTML and CSS. We use the same Grunt plugins as in the chat widget project, however there are some more:
- handlebars — a Grunt task for working with Handlebars templates.
- snockets — lets us forget about including necessary files in the given order. Priceless for projects with 50+ source files.
We’ve also built a custom plugin for calculating MD5 hashes of deployed files. Using this solution, the browser will only download the files that have changed compared to the previous version. In a nutshell, it works like this:
# MD5 module for node.js: https://github.com/pvorb/node-md5
md5 = require 'MD5'
calculateMD5String = (path) ->
'-' + md5 fs.readFileSync path
filepath = "app" + calculateMD5String("app.js") + ".js"
# filepath is now "app-f78b7292153b991a52b65c1115451118.js"
Again, deploying the application to all customers is as simple as:
grunt deploy:production
You won’t find a better way to work with your JavaScript projects.