2

I want to have a gruntfile with 2 tasks: less (compiles all less files) and watch (listens to changes and re-compiles the changed file).

I have the following Gruntfile.js:

module.exports = function(grunt) {
    var files = [
        {
            expand: true,
            cwd: 'media/less',
            src: ['*.less'],
            dest: 'media/css/',
            ext: '.css'
        },
        {
            expand: true,
            cwd: 'media/less/vendor',
            src: ['*.less'],
            dest: 'media/css/vendor/',
            ext: '.css'
        },
        {
            expand: true,
            cwd: 'media/admin/less',
            src: ['*.less'],
            dest: 'media/admin/css/',
            ext: '.css'
        }
    ];

    grunt.initConfig({
        less: {
            development: {
                options: {
                    compress: false,
                    yuicompress: true,
                    optimization: 2
                },
                files: files
            },
            production: {
                options: {
                    compress: true,
                    yuicompress: true,
                    optimization: 2
                },
                files: files
            }
        },
        watch: {
            styles: {
                files: ['media/**/*.less'],
                tasks: ['less:development'],
                options: {
                    nospawn: true
                }
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-less');
    grunt.loadNpmTasks('grunt-contrib-watch');

    grunt.registerTask('default', ['less:development']);
};

The less task runs correctly without any problems. The watch task however listens to changes, but re-compiles all files when one is changed. I suspect it's something to do with how I set up my less task, because I want my less file list to be dynamic and not to add each file manually.

As per this answer grunt should already support this, but I'm unsure how.

Community
  • 1
  • 1
Eduard Luca
  • 6,514
  • 16
  • 85
  • 137

2 Answers2

1

Ended up using the watch event and overriding the files property of the less task. Here's my final code:

module.exports = function(grunt) {
    var files = [
        {
            expand: true,
            cwd: 'media/less',
            src: ['*.less'],
            dest: 'media/css/',
            ext: '.css',
            extDot: 'last'
        },
        {
            expand: true,
            cwd: 'media/less/vendor',
            src: ['*.less'],
            dest: 'media/css/vendor/',
            ext: '.css',
            extDot: 'last'
        },
        {
            expand: true,
            cwd: 'media/admin/less',
            src: ['*.less'],
            dest: 'media/admin/css/',
            ext: '.css',
            extDot: 'last'
        }
    ];

    grunt.initConfig({
        less: {
            development: {
                options: {
                    compress: false,
                    yuicompress: true,
                    optimization: 2
                },
                files: files
            },
            production: {
                options: {
                    compress: true,
                    yuicompress: true,
                    optimization: 2
                },
                files: files
            }
        },
        watch: {
            styles: {
                files: ['media/**/*.less'],
                tasks: ['less:development'],
                options: {
                    nospawn: true
                }
            }
        }
    });

    grunt.event.on('watch', function(action, filepath){
        // ignore include files, TODO: have naming convention
        // if an include file has been changed, all files will be re-compiled
        if(filepath.indexOf('.inc.') > -1)
            return true;

        // might not be the most efficient way to do this
        var srcDir = filepath.split('/');
        var filename = srcDir[srcDir.length - 1];
        delete srcDir[srcDir.length - 1];
        srcDir = srcDir.join('/');
        var destDir = srcDir.replace(/less/g, 'css');

        grunt.config('less.development.files', [{
            src: filename,
            dest: destDir,
            expand: true,
            cwd: srcDir,
            ext: '.css',
            extDot: 'last'
        }]);
    });

    grunt.loadNpmTasks('grunt-contrib-less');
    grunt.loadNpmTasks('grunt-contrib-watch');

    grunt.registerTask('default', ['less:development']);
};
Eduard Luca
  • 6,514
  • 16
  • 85
  • 137
0

I'm unsure what you mean with the current title of your question Grunt watch less on changed file only. Do you mean that is a problem? That's the expected behavior of a watch task, it will watch the files specified for changes and run the tasks you specified - in this case the LESS compilation.

I did some changes to your file. Some of it was simplified, some was changed keeping flexibility and expandability of the script in mind.

First install underscore as a dependency, by running:

npm install underscore --save-dev

Then do the following changes to your Gruntfile.js:

module.exports = function(grunt) {

    var _ = require('underscore');

    var files = {
        app : {
            '<%= path.styles.css %>/styles.css' : '<%= path.styles.less %>/*.less'
        },
        vendor : {
            '<%= path.styles.css %>/styles-vendor.css' : '<%= path.styles.vendor %>/*.less'
        },
        admin : {
            '<%= path.styles.css %>/styles-admin.css' : '<%= path.styles.admin %>/*.less'
        }
    }

    function all() {
        'use strict';
        var allfiles = {},
            i = {};

        for (i in files) {
            _.extend(allfiles, files[i]);
        }
        return allfiles;
    }

    grunt.initConfig({
        path : {
            media : 'media',
            styles : {
                css: 'media/css',
                less: 'media/less',
                admin: 'media/admin/less',
                vendor: '<%= path.styles.less %>/vendor'                
            }
        },
        less: {
            development: {
                options: {
                    compress: false,
                    yuicompress: true,
                    optimization: 2
                },
                files: (all())
            },
            production: {
                options: {
                    compress: true,
                    yuicompress: true,
                    optimization: 2
                },
                files: (all())
            }
        },
        watch: {
            styles: {
                files: ['<%= path.media %>/**/*.less'],
                tasks: ['less:development'],
                options: {
                    nospawn: true
                }
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-less');
    grunt.loadNpmTasks('grunt-contrib-watch');

    // run several tasks as default (handy for complex projects)
    grunt.registerTask('dist', [ // run with 'grunt dist'
        'less:production'
    ]);
    grunt.registerTask('dev', [ // default, will run with 'grunt' only
        'less:development'
    ]);

    grunt.registerTask('default', 'dev');
};

If what you want is to actually compile the sets of files separately (files.app, files.vendor & files.admin), you might need to split the task some more, like so:

        less: {
            app: {
                options: {
                    compress: false,
                    yuicompress: true,
                    optimization: 2
                },
                files: files.app
            },
            vendor: {
                options: {
                    compress: true,
                    yuicompress: true,
                    optimization: 2
                },
                files: files.vendor
            },
            admin: {
                options: {
                    compress: true,
                    yuicompress: true,
                    optimization: 2
                },
                files: files.admin
            },
            development: {
                options: {
                    compress: false,
                    yuicompress: true,
                    optimization: 2
                },
                files: (all())
            },
            production: {
                options: {
                    compress: true,
                    yuicompress: true,
                    optimization: 2
                },
                files: (all())
            }
        },
        watch: {
            all: {
                files: ['<%= path.media %>/**/*.less'],
                tasks: ['less:development'],
                options: {
                    nospawn: true
                }
            },
            app : {
                files: ['<%= path.styles.less %>/*.less'],
                tasks: ['less:app'],
                options: {
                    nospawn: true
                }
            },
            vendor : {
                files: ['<%= path.styles.vendor %>/*.less'],
                tasks: ['less:vendor'],
                options: {
                    nospawn: true
                }
            },
            admin : {
                files: ['<%= path.styles.admin %>/*.less'],
                tasks: ['less:admin'],
                options: {
                    nospawn: true
                }
            }
        }

Then you could then run either of these:

grunt watch:app
grunt watch:vendor
grunt watch:admin

You can always to choose to run the tasks directly, once:

grunt less:app
grunt less:vendor
grunt less:admin

Hope this helps! Please note that I haven't tested this.

Wallace Sidhrée
  • 11,221
  • 6
  • 47
  • 58
  • Not really what I want, although not far. So the desired flow: I change one file, that one file gets compiled. The current flow: I change one file, all files get re-compiled. With your solution (the 2nd one), both the `less:development` task and the `less:app` task run. Also, I don't also want to bundle the LESS files into one single CSS file, I want to keep the file structure the same. – Eduard Luca Apr 02 '14 at 14:23
  • I've reproduced your setup and could not solve the puzzle. The SO you mentioned points to two issues raised at different projects that try to deal with the same problem [[1](https://github.com/gruntjs/grunt-contrib-watch/issues/14)] and [[2](https://github.com/shama/gaze/issues/14)], but that haven't been 100% solved. At this point, I think `LESS` tasks are folder based, as are `Compass` tasks (for `SASS`). In the case of `Compass` they even mention it explicitly [on their readme](https://github.com/gruntjs/grunt-contrib-compass). – Wallace Sidhrée Apr 02 '14 at 21:24
  • Managed to solve it. Took about 3 hours, but hopefully it works correctly now. See my answer to see what I did. – Eduard Luca Apr 03 '14 at 09:10