Articles

Stylus with a gulp: A fantastically fast front-end workflow

By Wunderer

In this post, we’ll be taking a look at one solution to the aforementioned problem: slow compile times and multiple dependencies.

First, let’s take a look at the most obvious 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 like 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 amongst 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 evokes 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 let’s 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:

  1. 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).

  2. 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

  3. 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.info
    

    You’ll notice that in this case, I’m using a couple of assumptions.

    1. You’ll be using Display Suite (because you should!)
    2. 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:

    1. aggregated_js
      Gulp will lint, concatenate and uglify whatever scripts it finds in the js folder (or its subfolders) into a neat little file called scripts.min.js and place that here.
    2. js
      See point 1.
    3. 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, see prefix task definition in the gulpfile.js)
    4. img
      We’ll keep theme image files here.
    5. styles
      Sources for stylus.Files with the extension .styl found immediately inside the styles the directory will result in their respective compiled equivalents in CSS form in the css directory, anything in the globals the 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 the modules subdirectory whichever way you please, including creating your own subdirectories for ease of use or reading, they will all be automatically included at compile time.

  4. Now that we’ve done the preparatory work, how do we actually make this whole thing work?
    Easy:

    1. From the theme root directory run: npm install (assuming you have Node.js already installed on your system)
    2. 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!
    3. 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 the 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 modifying a relevant line of gulpfile.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
    4. What about those ds_layouts and panel_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 available to both front, and back ends. You’ll be able to view the layout of your page in its near-completed form when working with page manager, for example.

If you’ve managed to follow along and everything 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.