← All Articles A Product of Kinsa Creative

Sass, CSS and JS with Gulp

Concatenate and minify Scss and JS with Gulp.

An aside if using VSCode as an editor, development can be expedited by triggering the watch task by enabling auto save. In Settings, search for "auto save" and enable Files to Auto Save onFocusChange or afterDelay. For editing side-by-side with rendered output, afterDelay is probably the way to go. For editing full-screen, onFocusChange will save when switching from the editor window to the browser window.

Install the Gulp CLI globally:

npm install gulp-cli -g

Install Sass, and Gulp-Sass to compile Scss; Autoprefixer, postcss, postcss-scss, and gulp-postcss to apply CSS browser prefixing; gulp-minify, gulp-rename, and gulp-concat to concatenate and minify JS files; gulp-sourcemaps to create .map files:

npm install sass gulp-sass autoprefixer gulp-minify gulp-rename gulp-minify gulp-sourcemaps postcss postcss-scss gulp-postcss --save-dev

More on sourcemaps can be found in this Google article.

Create a gulpfile in the root of the project. It will watch for changes to .scss or .sass files in the ./src/scss/ directory and changes to .js files in the ./src/js/ directory, compiling the resulting minified scripts, styles, and sourcemaps to the ./dist/ directory:

touch gulpfile.js

Add to that:

'use strict';

const gulp = require( 'gulp' );
const autoprefixer = require( 'autoprefixer' )
const postcss = require( 'gulp-postcss' )
const sass = require( 'gulp-sass' )( require( 'sass' ) );
const sourcemaps = require( 'gulp-sourcemaps' )
const minify = require( 'gulp-minify' );
const concat = require( 'gulp-concat' );
const rename = require( 'gulp-rename' );

const compileSass = () => {
    // any Scss or Sass files
    return gulp.src( [ './src/scss/*.s[ac]ss' ] )
        // compress the result to reduce file size
        .pipe( sass( { outputStyle: 'compressed' } ).on( 'error', sass.logError ) )
        // point to the directory where the result should be saved; the resulting CSS file will have the same filename as the Sass file but with the .css file extension
        .pipe( gulp.dest( './dist' ) );
}

exports.compileSass = compileSass;

const concatJS = () => {
    return gulp.src( [ './src/js/*.js' ] )
            // concat any JS files in `./src/js/` to a file called `all.js`
            .pipe( concat( 'all.js' ) )
            .pipe( sourcemaps.init() )
            // minify `all.js`; creates a file called `all-min.js` without further args specifying otherwise
            // this will tell it instead to make a file named `all.min.js`
            .pipe(
                    minify( {
                        ext: '.min.js',
                        noSource: true  // write only the `.min.js` file
                    } )
            )
            // save the sourcemaps to a separate file (rather than adding the map to the minified JS file itself)
            .pipe( sourcemaps.write( './' ) )
            // save the concatenated, minified file to the `/dist` directory
            .pipe( gulp.dest( './dist' ) );
}

exports.concatJS = concatJS;

exports.watch = () => {
    gulp.watch( './src/scss/*.s[ac]ss', compileSass );
    gulp.watch( './src/js/*.js', concatJS );
};

const minimizeStaticAssets = ( cb ) => {
    compileSass();
    minifyJS();
    cb();
}

exports.default = minimizeStaticAssets;

Autoprefixer requires a browserlist file in the root of the project:

touch .browserslistrc

Add to that:

# https://github.com/twbs/bootstrap/blob/v5.3.3/.browserslistrc
# https://github.com/browserslist/browserslist#readme

>= 0.5%
last 2 major versions
not dead
Chrome >= 60
Firefox >= 60
Firefox ESR
iOS >= 12
Safari >= 12
not Explorer <= 11

During development, gulp will recreate the compressed files every time the source is changed via the gulp watch task. Run gulp alone to run the default action and recreate the compressed files once, or run each task independently as gulp compileSass or gulp minifyJS.

Minimize a single CSS and JS file

For a simpler site, CSS and JS can be compressed from a single source file.

Create a gulpfile in the root of the project. It will watch for changes to ./static/main.css and ./static/main.js, compiling the resulting minified scripts, styles, and sourcemaps in the same directory:

'use strict';

const gulp = require( 'gulp' );
const autoprefixer = require( 'autoprefixer' )
const postcss = require( 'gulp-postcss' )
const sass = require( 'gulp-sass' )( require( 'sass' ) );
const sourcemaps = require( 'gulp-sourcemaps' )
const minify = require( 'gulp-minify' );
const rename = require( 'gulp-rename' );

const compressCSS = () => {
    // point to the CSS file
    return gulp.src( './static/main.css' )
            .pipe( sourcemaps.init() )
            .pipe( postcss( [ autoprefixer() ] ) )
            // compress the CSS to reduce file size
            .pipe( sass( { outputStyle: 'compressed' } ).on( 'error', sass.logError ) )
            .pipe( rename( 'main.min.css' ) )
            .pipe( sourcemaps.write( '.' ) ); // passing a path tells it to write to a separate .map file rather than embedding it in the compressed CSS file
            .pipe( gulp.dest( './static' ) );
}

exports.compressCSS = compressCSS;

const minifyJS = () => {
    return gulp.src( './static/main.js' )
            .pipe( sourcemaps.init() )
            .pipe(
                    minify( {
                        ext: '.min.js',
                        noSource: true  // write only the `.min.js` file
                    } )
            )
            .pipe( sourcemaps.write( '.' ) ); // passing a path tells it to write to a separate .map file rather than embedding it in the minified JS file
            .pipe( gulp.dest( './static' ) );
}

exports.minifyJS = minifyJS;

exports.watch = () => {
    gulp.watch( './static/main.css', compressCSS );
    gulp.watch( './static/main.js', minifyJS );
};

const minimizeStaticAssets = ( cb ) => {
    compressCSS();
    minifyJS();
    cb();
}

exports.default = minimizeStaticAssets;

Caveats

Minify is known to blow up when it encounters ES6 optional chaining, ?, as in document.querySelector( 'foo' )?.doSomethingWithFoo() which requires rewriting the optional using an if statement to check if foo exists before proceeding e.g.

const bar = document.querySelector( 'foo' );
if ( bar ) {
    bar.doSomethingWithFoo();
}

Feedback?

Email us at enquiries@kinsa.cc.