Paint the Web - a micro-Blog in .md about Dev, Web and more

A Grunt quick start with a GruntFile boilerplate

This Grunt quickstart contains the following features:

  • Sass to CSS transpiling (*.scss)
  • CSS compatibility optimizations and minifing
  • JS concating of files and folders
  • JS mangling and minifing
  • Image optimizations
  • Automatic file change watchers
  • JS: ES5.1 support, but the GruntFile.js is written in ES6
  • Sass: node-sass usage, a lot faster then Ruby Sass implementation
  • (sassdoc HTML Generator)
  • (Google PageSpeed analyse within shell)

Prepare System

Install NodeJS, it comes bundled with the package manager npm.

If you use packages from packagist you also need composer, most frontend libraries are available from bower or other package systems. Bower is available through npm and will be installed in the next step.

In the boilerplate are variables for setting the path to the bower and composer path, relative from the GruntFile.js folder. You could use them for writing less code and when you have a few frameworks you need to work with, with different positions of all folders, you could simplify that with adding more custom folders like path_bower and path_bower.

Install Packages

Now open your shell and install the globally needed packages with npm:

npm i -g node-sass
npm i -g sassdoc
npm i -g grunt-cli
npm i -g bower

cd to your project folder and execute:

npm init

for initializing npm in your project, this will create a package.json, add the next code block to devDependencies.

If you don't want to publish the project in npm at the moment, you could also just create the package.json and add that content:

{
  "devDependencies": {
    "autoprefixer": "^7.1.1",
    "grunt": "^1.0.1",
    "grunt-concurrent": "^2.3.1",
    "grunt-contrib-concat": "^1.0.1",
    "grunt-contrib-cssmin": "^2.0.0",
    "grunt-contrib-imagemin": "^1.0.1",
    "grunt-contrib-uglify": "^2.2.0",
    "grunt-contrib-watch": "^1.0.0",
    "grunt-pagespeed": "^2.0.1",
    "grunt-postcss": "^0.8.0",
    "grunt-sass": "^2.0.0",
    "imagemin-mozjpeg": "^5.1.0",
    "load-grunt-tasks": "^3.5.2",
    "pixrem": "^3.0.2",
    "time-grunt": "^1.4.0"
  }
}

Now run:

npm install

Keeping it up-to-date is easy with npm update.

Then fetch bower packages used in this example:

Create bower.json with this content:

{
  "name": "name"
}

and run:

bower install --save-dev jQuery
bower install --save-dev normalize-css
bower install --save-dev font-awesome

GruntFile.js

The datastructure used is the same as within the PaintTheWeb repo.

Development files:

asset/
asset/js/
asset/js/manual-concat-file.js
asset/js/src/
asset/js/src/automatic-concat-file.js
asset/style/
asset/style/main.scss
asset/media/

Output Files / To CDN:

view/out/
view/out/js.js
view/out/js.min.js
view/out/style.css
view/out/style.min.css
view/out/media/

Create the GruntFile.js with following content, configure the paths:

There should be a little bit more explanation of the logic...

module.exports = function(grunt) {
    //
    // Paths
    //

    /**
     * with trailing slash
     * bower_components/ (mostly)
     * @type {string}
     */
    let path_bower = 'bower_components/';
    /**
     * with trailing slash
     * vendor/ (mostly)
     * @type {string}
     */
    let path_composer = 'vendor/';

    /**
     * This folder will be watched and JS concated, mangled, minified
     * 
     * with trailing slash
     * @type {string}
     */
    let path_js_src_dir = 'asset/js/src/';
    /**
     * The folder in which the JS output should be saved
     * 
     * with trailing slash
     * @type {string}
     */
    let path_js_build_dir = 'view/out/';

    /**
     * The main Sass file that should be transpiled, but:
     * 
     * without extension
     * @type {string}
     */
    let path_sass_src_file = 'asset/style/main';
    /**
     * The folder where most sass files are located, will be used for the CSS file watcher
     * 
     * with trailing slash
     * @type {string}
     */
    let path_sass_src_dir = 'asset/style/';
    /**
     * The folder in which the CSS should be saved
     * 
     * with trailing slash
     * @type {string}
     */
    let path_sass_build_dir = 'view/out/';
    /**
     * Name of the CSS file, but:
     * 
     * without extension
     * @type {string}
     */
    let path_sass_build_file = 'style';

    /**
     * The source image folder, will be watched and all images optimized and copied into path_img_build 
     *
     * with trailing slash
     * @type {string}
     */
    let path_img_src = 'asset/media/';
    /**
     * The folder in which the optimized images are saved
     * 
     * with trailing slash
     * @type {string}
     */
    let path_img_build = 'view/out/media/';

    //
    // JS concat
    //

    let js_concat = [
        path_bower + 'jQuery/dist/jquery.min.js',
        path_js_src_dir + '**/*.js'
    ];

    //
    // Options
    //

    /**
     * imagemin level of optimization for png and dynamic (svg|gif)
     * @type {number}
     */
    let img_optimization_lvl = 3;
    /**
     * imagemin level of builded image quality for jpeg and dynamic (svg|gif)
     * @type {number}
     */
    let img_quality_lvl = 90;

    //
    // Watcher
    //

    /**
     * The more files must be scanned the longer it takes, keep the list clean!
     * @type {[*]}
     */
    let watch_css = [
        path_sass_src_dir + '**/*.scss',
        '!**/node_modules/**',
        '!**/*.min.css'
    ];
    /**
     * The more files must be scanned the longer it takes, keep the list clean!
     * @type {[*]}
     */
    let watch_js = [
        path_js_src_dir + '**/*.js',
        '!**/node_modules/**',
        '!**/*.min.js'
    ];
    /**
     * The more files must be scanned the longer it takes, keep the list clean!
     * @type {[*]}
     */
    let watch_img = [
        path_img_src + '**/*.{gif,svg,png,jpg}',
    ];

    require('time-grunt')(grunt);

    require('load-grunt-tasks')(grunt);

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        // JS
        concat: {
            dist: {
                // warns when something was not found but was specified
                nonull: true,
                src: js_concat,
                dest: path_js_build_dir + 'js.js'
            }
        },
        uglify: {
            build: {
                options: {
                    sourceMap: true,
                    mangle: {
                        properties: true,
                        toplevel: false,
                        reserved: ['jQuery', 'jquery']
                    }
                },
                src: path_js_build_dir + 'js.js',
                dest: path_js_build_dir + 'js.min.js'
            }
        },

        // CSS
        sass: {
            options: {
                sourceMap: true
            },
            dist: {
                files: {
                    [path_sass_build_dir + path_sass_build_file + '.css']: path_sass_src_file + '.scss'
                }
            }
        },
        cssmin: {
            target: {
                files: [{
                    expand: true,
                    cwd: path_sass_build_dir,
                    src: [path_sass_build_file + '.css', '!' + path_sass_build_file + '.css.map'],
                    dest: path_sass_build_dir,
                    ext: '.min.css'
                }]
            }
        },
        postcss: {
            options: {
                map: false,
                processors: [
                    require('pixrem')(), // add fallbacks for rem units
                    require('autoprefixer')({browsers: 'last 4 versions'})
                ]
            },
            dist: {
                src: path_sass_build_dir + path_sass_build_file + '.min.css'
            }
        },

        // Image
        imagemin: {
            png: {
                options: {
                    optimizationLevel: img_optimization_lvl
                },
                files: [{
                    expand: true,
                    cwd: path_img_src,
                    src: ['**/*.png'],
                    dest: path_img_build
                }]
            },
            jpg: {
                options: {
                    quality: img_quality_lvl,
                    progressive: true,
                    use: [require('imagemin-mozjpeg')()]
                },
                files: [{
                    expand: true,
                    cwd: path_img_src,
                    src: ['**/*.jpg'],
                    dest: path_img_build
                }]
            },
            dynamic: {
                options: {
                    optimizationLevel: img_optimization_lvl,
                    quality: img_quality_lvl,
                    svgoPlugins: [{removeViewBox: false}]
                },
                files: [{
                    expand: true,
                    cwd: path_img_src,
                    src: ['**/*.{gif,svg}'],
                    dest: path_img_build
                }]
            }
        },

        // Multi Tasking
        concurrent: {
            image: ['imagemin:png', 'imagemin:jpg', 'imagemin:dynamic'],
            build: [['js'], ['css'], 'concurrent:image']
        },

        // JS and CSS/Sass file watcher
        watch: {
            css: {
                files: watch_css,
                tasks: ['css']
            },
            js: {
                files: watch_js,
                tasks: ['js']
            },
            image: {
                files: watch_img,
                tasks: ['image']
            }
        }
    });

    // Multi-Thread Task Runner
    grunt.loadNpmTasks('grunt-concurrent');

    // JS
    grunt.registerTask('js', ['concat', 'uglify']);

    // SASS
    grunt.registerTask('css', ['sass', 'cssmin', 'postcss']);

    // Images
    grunt.registerTask('image', ['concurrent:image']);

    // Build All
    grunt.registerTask('build', ['concurrent:build']);
};

Grunt Execution

Now you could simply run grunt with the following commands. There must be the main.scss file and some JS file in path_js_src_dir that no error will be displayed. Images are optional.

Executes the Sass -> CSS task:

grunt css

Executes the JS task:

grunt js

Executes the Image task:

grunt image

Executes Sass/CSS, JS and Images at once:

grunt build

File Watcher

A File watcher will run the specified task/s when a file has been changed, you could simply start one with

grunt watch css

if you just want to let it run when you change a Sass (*.scss) file.

Or let it watch for everything:

grunt watch

As all images will be changed everytime, depending on your deployment it could be that you should exclude images from the watchers. Also it could take a lot of time when you got a lot of pictures. So run grunt image when you are finished with handling images.

If you use PHPStorm as IDE, you should execute from the GruntTasks panel. Right-click on the GruntFile.js -> "show Grunt Tasks". Only with this the virtual file system that PHPStorm uses will be refreshed and things like automatic upload are working.