Stylus with Gulp: a fantastically fast front-end workflow.
Modern CSS preprocessors are hardly a surprise to anybody these days, SASS and LESS have polarized the developer ecosystem and workflow tools like Grunt and Gulp have become welcome additions to both camps. Not so welcome additions to this new reality are compile times that in complex projects can stretch into seconds, sometimes in the double digits, and multiple dependency trees. How does a compile time of milliseconds with exactly one dependency sound? Intrigued? Then keep reading!
In this post we'll be taking a look at one solution to the aforementioned problem: slow compile times and multiple dependencies.
First, lets take a look at the most obious solution to speeding things up: eliminating layers in our workflow stack. Say, we use Grunt (who doesn't!) or even better, Gulp (awesome!). What dependency is required to run those awesome things? Node.js, of course. Now, wouldn't it be great if our preprocessor also ran on Node, and maybe its extensions and packages too? Never fear, Stylus is here!Stylus is the underdog and player number three in what at first glance appears to be a two-horse race.While Stylus does not have as large a collection of extensions as the other two, the ones that are available are top-notch and cover all the bases. There's the nib library for all things CSS3, Rupture for media query handling, and an absolutely fantastic Jeet for a grid system amongs others.
Stylus won't strongarm you into any kind of a specific syntax style. You can use standard CSS syntax on one extreme, or white-space delimited syntax on the other and everything in between. With great power, of course, comes great responsibility, so unless you enforce a certain discipline, there is potential for some not-so-pretty code.
Next thing, Grunt or Gulp? If neither of those words evoke a strong sense of tribal belonging, then go with Gulp, otherwise... look into Gulp and see if you can live with it. We'll be setting up a killer workflow based on Gulp right here.
So lets get right down to business: lets set up a real-world Drupal theme development environment that you'll be able to use right away.
tl;dr scroll down to the bottom and download a package that contains everything described here, otherwise:
- Remember that single dependency? What does it mean exactly? It means that there is only one file to maintain for versioning all our packages and tools: node's
package.json
, here is mine:{
"name": "project_name",
"version": "0.0.0",
"dependencies": {
"stylus": "^0.52.0",
"stylus-type-utils": "0.0.3"
},
"devDependencies": {
"bower": "^1.4.1",
"browser-sync": "^2.8.1",
"gulp": "^3.9.0",
"gulp-autoprefixer": "^2.3.1",
"gulp-concat": "^2.6.0",
"gulp-jshint": "^1.11.2",
"gulp-rename": "^1.2.2",
"gulp-sourcemaps": "^1.5.2",
"gulp-strip-debug": "^1.0.2",
"gulp-stylus": "^2.0.4",
"gulp-uglify": "^1.2.0",
"jeet": "^6.1.2",
"jshint-stylish": "^2.0.1",
"nib": "^1.1.0",
"rupture": "^0.6.1",
"yargs": "^3.18.0"
},
"scripts": {
"postinstall": "find node_modules/ -name '*.info' -type f -delete"
}
}Place this in your theme's root (name it
package.json
). - Configure Gulp:
gulpfile.js
, here is mine:'use strict';
var gulp = require('gulp');
var prefix = require('gulp-autoprefixer');
var uglify = require('gulp-uglify');
var jshint = require('gulp-jshint');
var concat = require('gulp-concat');
var stylish = require('jshint-stylish');
var rename = require('gulp-rename');
var stripDebug = require('gulp-strip-debug');
var browserSync = require('browser-sync').create();
var stylus = require('gulp-stylus');
var sourcemaps = require('gulp-sourcemaps');
var reload = browserSync.reload;
var args = require('yargs').argv;
var nib = require('nib');var serverUrl = args.proxy;
if (!serverUrl) {
serverUrl = 'local.example.dev';
}// Confingure our directories
var paths = {
js: 'js/**/*.js',
jsDest: 'aggregated-js',
css: 'css',
styles: 'styles',
ds: 'ds_layouts',
panels: 'panel_layouts',
img: 'img',
};//////////////////////////////
// Begin Script Tasks
//////////////////////////////
gulp.task('lint', function () {
return gulp.src([
paths.js
])
.pipe(jshint())
.pipe(jshint.reporter(stylish))
});gulp.task('scripts', function() {
return gulp.src(paths.js)
// Concatenate everything within the JavaScript folder.
.pipe(concat('scripts.js'))
.pipe(gulp.dest(paths.jsDest))
.pipe(rename('scripts.min.js'))
// Strip all debugger code out.
.pipe(stripDebug())
// Minify the JavaScript.
.pipe(uglify())
.pipe(gulp.dest(paths.jsDest));
});//////////////////////////////
// Stylus Tasks
//////////////////////////////
gulp.task('styles', function () {
gulp.src(paths.styles + '/*.styl')
.pipe(sourcemaps.init())
.pipe(stylus({
paths: ['node_modules', 'styles/globals'],
import: ['jeet/stylus/jeet', 'stylus-type-utils', 'nib', 'rupture/rupture', 'variables', 'mixins'],
use: [nib()],
'include css': true
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.css))
.pipe(browserSync.stream());
});gulp.task('ds', function () {
gulp.src(paths.ds + '/**/*.styl')
.pipe(sourcemaps.init())
.pipe(stylus({
paths: ['node_modules', 'styles/globals'],
import: ['jeet/stylus/jeet', 'stylus-type-utils', 'nib', 'rupture/rupture', 'variables', 'mixins'],
use: [nib()]
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.ds))
.pipe(browserSync.stream());
});gulp.task('panels', function () {
gulp.src(paths.panels + '/**/*.styl')
.pipe(sourcemaps.init())
.pipe(stylus({
paths: ['node_modules', 'styles/globals'],
import: ['jeet/stylus/jeet', 'stylus-type-utils', 'nib', 'rupture/rupture', 'variables', 'mixins'],
use: [nib()]
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.panels))
.pipe(browserSync.stream());
});//////////////////////////////
// Autoprefixer Tasks
//////////////////////////////
gulp.task('prefix', function () {
gulp.src(paths.css + '/*.css')
.pipe(prefix(["last 8 version", "> 1%", "ie 8"]))
.pipe(gulp.dest(paths.css));
});//////////////////////////////
// Watch
//////////////////////////////
gulp.task('watch', function () {
gulp.watch(paths.js, ['lint', 'scripts']);
gulp.watch(paths.styles + '/**/*.styl', ['styles']);
gulp.watch(paths.ds + '/**/*.styl', ['ds']);
gulp.watch(paths.panels + '/**/*.styl', ['panels']);
gulp.watch(paths.styles + '/globals/**/*.styl', ['styles', 'ds', 'panels']);
});//////////////////////////////
// BrowserSync Task
//////////////////////////////
gulp.task('browserSync', function () {
browserSync.init({
proxy: serverUrl
});
});//////////////////////////////
// Server Tasks
//////////////////////////////
gulp.task('default', ['scripts', 'watch', 'prefix']);
gulp.task('serve', ['scripts', 'watch', 'prefix', 'browserSync'])Place it in your theme's root, name it
gulpfile.js
- Now, the
gulpfile.js
above expects things to be in certain places, let's not disappoint it!Create a file structure like this: (if you download the package at the bottom of the page, you'll have it all set)
theme_name
|\
| ds_layouts
|\
| panel_layouts
|\
| aggregated_js
|\
| js
|\
| css
|\
| img
|\
| styles
| |\
| | globals
| |\
| | modules
| \
| includes
| style.styl
\
theme_name.infoYou'll notice that in this case I'm using a couple of assumptions.
- You'll be using Display Suite (because you should!)
- You'll be using Panels (because why not)
While Display Suite, once enabled, will look for a folder named ds_layouts in your theme, Panels needs to be informed of our panel_layouts directory. To do so, add this line somewhere near the bottom of your
theme_name.info
file:plugins[panels][layouts] = panel_layouts
The rest of the directory structure is as follows:aggregated_js
Gulp will lint, concatenate and uglify whatever scripts it finds in thejs
folder (or its subfolders) into a neat little file calledscripts.min.js
and place that here.js
See point 1.css
Destination directory for compiled.styl
files and sourcemaps. The Gulp workflow will autoprefix the compiled files to be compatible with browser versions as specified in the gulp file (at the moment, 8 versions back from current, seeprefix
task definition in thegulpfile.js
)img
We'll keep theme image files here.-
styles
Sources for stylus.
Files with the extension.styl
found immediately inside thestyles
directory will result in their respective compiled equivalents in CSS form in thecss
directory, anything in theglobals
subdirectory will be included during compile time with any.styl
file (even the files in ds_layouts and panels_layouts! Do you see where this is going?) so naturally it's a good spot to put our variable definitions, mixins, extendables and such things.Modules
subdirectory contains partials to be included in the main file(s) (in our case,style.styl
), they won't be included automatically, so style.styl file must contain the following at its beginning (and ideally, that would be all it contains):@import 'includes/*.css'
@import 'modules/**/*.styl'Includes
subdirectory will have any pre-compiled css files (such as html5reset.css) which will be rolled as the first thing into compiled css. You can organize the structure in themodules
subdirectory whichever way you please, incuding creating your own subdirectories for ease of use or reading, they will all be automatically incuded at compile time.
- Now that we've done the preparatory work, how do we actually make this whole thing work?Easy:
- From the theme root directory run:
npm install
(assuming you have Node.js already installed on your system) - From the theme root, run:
gulp
Gulp will now listen for changes to your js or styl files and promptly compile them into css and minimized js for you! Congratulations! - Additionally, you could take advantage of the awesome BrowserSync and run:
gulp serve
This will do the same as point number 2 above, but will also run a BrowserSync server which will reload the browsers (and other devices connected to the site) upon detecting a change to your CSS or JS files and will sync events on page (including scrolling and hovering!). By default, it will proxy a default site found at "local.example.dev" and re-broadcast it, if you will, at localhost:3000. You can change the default proxy by modifyling relevant line ofgulpfile.js
(search for local.example.dev, it is there only once), or you can specify your own proxy like this:gulp serve --proxy=local.mysite.dev:8888
- What about those
ds_layouts
andpanel_layouts
directories, what do those have to do with stylus you ask? They are there to make your life easier when working with ds and panels templates. Both of those have an option to include css file with the template, and if you were to place a stylus file with the appropriate filename in the template subdirectory, Gulp will compile that file using the global includes (so your mixins, variables, your grid and breakpoint will all be available to you!), and place it in the same place as source for you to include in the template! This will make the styles defined there availabe to both front, and back ends. You'll be able to view the layout of your page in its near compelted form when working with page manager, for example.
- From the theme root directory run:
If you've managed to follow along and everthing worked, wonderful! I've used node version 0.12.7 with latest available versions of all node packages to write this setup. If something does not work, download and extract the package below. It is a barebones Drupal theme ready to be set as a subtheme of an existing theme or act as standalone. It is also possible that Gulp wants to be installed globally, in that case, simply run npm install -g gulp
from the theme root.